heymenn commited on
Commit
6f9eaf4
·
1 Parent(s): d9ebd80

add versioning selection in the back and front end

Browse files
Files changed (6) hide show
  1. app.py +40 -8
  2. classes.py +39 -12
  3. schemas.py +16 -4
  4. static/script.js +22 -2
  5. static/style.css +15 -0
  6. templates/index.html +10 -2
app.py CHANGED
@@ -115,11 +115,37 @@ def get_tdoc_url(doc_id):
115
  return tdoc["url"]
116
  return "Document not indexed (re-indexing documents ?)"
117
 
118
- def get_spec_url(document):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  series = document.split(".")[0].zfill(2)
120
  url = f"https://www.3gpp.org/ftp/Specs/archive/{series}_series/{document}"
121
  versions = get_docs_from_url(url)
122
- return url + "/" + versions[-1] if versions != [] else f"Specification {document} not found"
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  def get_document(spec_id: str, spec_title: str, source: str):
125
  text = [f"{spec_id} - {spec_title}"]
@@ -230,14 +256,15 @@ def reconnect():
230
  def find_document(request: DocRequest):
231
  start_time = time.time()
232
  document = request.doc_id
 
233
  if valid_3gpp_doc_format.match(document):
234
  url = get_tdoc_url(document)
235
  elif valid_3gpp_spec_format.match(document):
236
- url = get_spec_url(document)
237
  elif valid_etsi_doc_format.match(document):
238
  url = etsi_doc_finder.search_document(document)
239
  elif valid_etsi_spec_format.match(document):
240
- url = etsi_spec_finder.search_document(document)
241
  elif document.startswith("GP"):
242
  for sp in gp_spec_locations:
243
  if document.lower() in sp.lower():
@@ -251,6 +278,10 @@ def find_document(request: DocRequest):
251
  version = None
252
  if valid_3gpp_spec_format.match(document):
253
  version = url.split("/")[-1].replace(".zip", "").split("-")[-1]
 
 
 
 
254
  scope = None
255
  spec_metadatas = spec_metadatas_3gpp if valid_3gpp_spec_format.match(document) else spec_metadatas_etsi
256
  for spec in spec_metadatas:
@@ -286,6 +317,7 @@ def find_document(request: DocRequest):
286
  def find_document_batch(request: BatchDocRequest):
287
  start_time = time.time()
288
  documents = request.doc_ids
 
289
  results = {}
290
  missing = []
291
 
@@ -293,11 +325,11 @@ def find_document_batch(request: BatchDocRequest):
293
  if valid_3gpp_doc_format.match(document):
294
  url = get_tdoc_url(document)
295
  elif valid_3gpp_spec_format.match(document):
296
- url = get_spec_url(document)
297
  elif valid_etsi_doc_format.match(document):
298
- etsi_doc_finder.search_document(document)
299
  elif valid_etsi_spec_format.match(document):
300
- etsi_spec_finder.search_document(document)
301
  elif document.startswith("GP"):
302
  for sp in gp_spec_locations:
303
  if document.lower() in sp.lower():
@@ -308,7 +340,7 @@ def find_document_batch(request: BatchDocRequest):
308
  missing.append(document)
309
  else:
310
  results[document] = url
311
-
312
  return BatchDocResponse(
313
  results=results,
314
  missing=missing,
 
115
  return tdoc["url"]
116
  return "Document not indexed (re-indexing documents ?)"
117
 
118
+ def version_to_3gpp(version: str) -> str:
119
+ """Convert semantic version string (e.g. '17.6.0') to 3GPP filename suffix (e.g. 'h60').
120
+ Returns the input unchanged if it does not look like a semantic version."""
121
+ parts = version.split(".")
122
+ if len(parts) != 3:
123
+ return version
124
+ try:
125
+ major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2])
126
+ except ValueError:
127
+ return version
128
+ if major < 10:
129
+ return version
130
+ letter = chr(ord('a') + major - 10)
131
+ return f"{letter}{minor}{patch}"
132
+
133
+ def get_spec_url(document, version: str = None):
134
  series = document.split(".")[0].zfill(2)
135
  url = f"https://www.3gpp.org/ftp/Specs/archive/{series}_series/{document}"
136
  versions = get_docs_from_url(url)
137
+ if not versions:
138
+ return f"Specification {document} not found"
139
+ if version:
140
+ suffix = version_to_3gpp(version)
141
+ # filename pattern: <specnumber>-<suffix>.zip
142
+ spec_number = document.replace(".", "")
143
+ target = f"{spec_number}-{suffix}.zip"
144
+ for v in versions:
145
+ if v.lower() == target.lower():
146
+ return url + "/" + v
147
+ # fallback to latest if requested version not found
148
+ return url + "/" + versions[-1]
149
 
150
  def get_document(spec_id: str, spec_title: str, source: str):
151
  text = [f"{spec_id} - {spec_title}"]
 
256
  def find_document(request: DocRequest):
257
  start_time = time.time()
258
  document = request.doc_id
259
+ requested_version = request.version
260
  if valid_3gpp_doc_format.match(document):
261
  url = get_tdoc_url(document)
262
  elif valid_3gpp_spec_format.match(document):
263
+ url = get_spec_url(document, requested_version)
264
  elif valid_etsi_doc_format.match(document):
265
  url = etsi_doc_finder.search_document(document)
266
  elif valid_etsi_spec_format.match(document):
267
+ url = etsi_spec_finder.search_document(document, requested_version)
268
  elif document.startswith("GP"):
269
  for sp in gp_spec_locations:
270
  if document.lower() in sp.lower():
 
278
  version = None
279
  if valid_3gpp_spec_format.match(document):
280
  version = url.split("/")[-1].replace(".zip", "").split("-")[-1]
281
+ elif valid_etsi_spec_format.match(document):
282
+ # extract version from the resolved URL path segment before the filename
283
+ parts = url.rstrip("/").split("/")
284
+ version = parts[-2] if len(parts) >= 2 else None
285
  scope = None
286
  spec_metadatas = spec_metadatas_3gpp if valid_3gpp_spec_format.match(document) else spec_metadatas_etsi
287
  for spec in spec_metadatas:
 
317
  def find_document_batch(request: BatchDocRequest):
318
  start_time = time.time()
319
  documents = request.doc_ids
320
+ requested_version = request.version
321
  results = {}
322
  missing = []
323
 
 
325
  if valid_3gpp_doc_format.match(document):
326
  url = get_tdoc_url(document)
327
  elif valid_3gpp_spec_format.match(document):
328
+ url = get_spec_url(document, requested_version)
329
  elif valid_etsi_doc_format.match(document):
330
+ url = etsi_doc_finder.search_document(document)
331
  elif valid_etsi_spec_format.match(document):
332
+ url = etsi_spec_finder.search_document(document, requested_version)
333
  elif document.startswith("GP"):
334
  for sp in gp_spec_locations:
335
  if document.lower() in sp.lower():
 
340
  missing.append(document)
341
  else:
342
  results[document] = url
343
+
344
  return BatchDocResponse(
345
  results=results,
346
  missing=missing,
classes.py CHANGED
@@ -100,7 +100,30 @@ class ETSISpecFinder:
100
  print(f"Error accessing {url}: {e}")
101
  return []
102
 
103
- def search_document(self, doc_id: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  # Example : 103 666[-2 opt]
105
  original = doc_id
106
 
@@ -108,17 +131,21 @@ class ETSISpecFinder:
108
  url2 = f"{self.second_url}/{self.get_spec_path(original)}/"
109
  print(url)
110
  print(url2)
111
-
112
  releases = self.get_docs_from_url(url)
113
- files = self.get_docs_from_url(url + releases[-1])
114
- for f in files:
115
- if f.endswith(".pdf"):
116
- return url + releases[-1] + "/" + f
117
-
 
 
118
  releases = self.get_docs_from_url(url2)
119
- files = self.get_docs_from_url(url + releases[-1])
120
- for f in files:
121
- if f.endswith('.pdf'):
122
- return url + releases[-1] + "/" + f
123
-
 
 
124
  return f"Specification {doc_id} not found"
 
100
  print(f"Error accessing {url}: {e}")
101
  return []
102
 
103
+ def _normalise_version(self, version: str) -> str:
104
+ """Normalise a user-supplied version string to ETSI zero-padded format.
105
+ '17.6.0' -> '17.06.00' (the '_60' release suffix is ignored during matching)
106
+ Already-normalised strings like '17.06.00' are returned unchanged."""
107
+ parts = version.strip("/").split(".")
108
+ if len(parts) == 3:
109
+ try:
110
+ return f"{int(parts[0]):02d}.{int(parts[1]):02d}.{int(parts[2]):02d}"
111
+ except ValueError:
112
+ pass
113
+ return version.strip("/")
114
+
115
+ def _pick_release(self, releases: list, version: str = None) -> str:
116
+ """Return the release folder matching version, or the latest if not found/specified."""
117
+ if version:
118
+ target = self._normalise_version(version)
119
+ for r in releases:
120
+ # folder names are like '17.06.00_60'; match on the part before '_'
121
+ folder = r.strip("/").split("_")[0]
122
+ if folder == target:
123
+ return r
124
+ return releases[-1]
125
+
126
+ def search_document(self, doc_id: str, version: str = None):
127
  # Example : 103 666[-2 opt]
128
  original = doc_id
129
 
 
131
  url2 = f"{self.second_url}/{self.get_spec_path(original)}/"
132
  print(url)
133
  print(url2)
134
+
135
  releases = self.get_docs_from_url(url)
136
+ if releases:
137
+ release = self._pick_release(releases, version)
138
+ files = self.get_docs_from_url(url + release)
139
+ for f in files:
140
+ if f.endswith(".pdf"):
141
+ return url + release + "/" + f
142
+
143
  releases = self.get_docs_from_url(url2)
144
+ if releases:
145
+ release = self._pick_release(releases, version)
146
+ files = self.get_docs_from_url(url2 + release)
147
+ for f in files:
148
+ if f.endswith(".pdf"):
149
+ return url2 + release + "/" + f
150
+
151
  return f"Specification {doc_id} not found"
schemas.py CHANGED
@@ -4,14 +4,20 @@ from typing import *
4
  class DocRequest(BaseModel):
5
  """
6
  Request model for single document retrieval.
7
-
8
  Used to specify which document or specification to retrieve by its unique identifier.
9
  """
10
  doc_id: str = Field(
11
- ...,
12
  title="Document Identifier",
13
  description="Unique identifier for the document or specification.",
14
  )
 
 
 
 
 
 
15
 
16
  class DocResponse(BaseModel):
17
  """
@@ -48,14 +54,20 @@ class DocResponse(BaseModel):
48
  class BatchDocRequest(BaseModel):
49
  """
50
  Request model for batch document retrieval.
51
-
52
  Allows retrieval of multiple documents in a single API call for efficiency.
53
  """
54
  doc_ids: List[str] = Field(
55
- ...,
56
  title="Document Identifier List",
57
  description="List of document identifiers to retrieve."
58
  )
 
 
 
 
 
 
59
 
60
  class BatchDocResponse(BaseModel):
61
  """
 
4
  class DocRequest(BaseModel):
5
  """
6
  Request model for single document retrieval.
7
+
8
  Used to specify which document or specification to retrieve by its unique identifier.
9
  """
10
  doc_id: str = Field(
11
+ ...,
12
  title="Document Identifier",
13
  description="Unique identifier for the document or specification.",
14
  )
15
+ version: Optional[str] = Field(
16
+ None,
17
+ title="Specification Version",
18
+ description="Desired version of the specification (e.g. '17.6.0'). Defaults to the latest version when omitted.",
19
+ examples=["17.6.0", "15.3.1", "18.0.0"]
20
+ )
21
 
22
  class DocResponse(BaseModel):
23
  """
 
54
  class BatchDocRequest(BaseModel):
55
  """
56
  Request model for batch document retrieval.
57
+
58
  Allows retrieval of multiple documents in a single API call for efficiency.
59
  """
60
  doc_ids: List[str] = Field(
61
+ ...,
62
  title="Document Identifier List",
63
  description="List of document identifiers to retrieve."
64
  )
65
+ version: Optional[str] = Field(
66
+ None,
67
+ title="Specification Version",
68
+ description="Desired version applied to all specification lookups in the batch (e.g. '17.6.0'). Defaults to latest when omitted.",
69
+ examples=["17.6.0", "15.3.1"]
70
+ )
71
 
72
  class BatchDocResponse(BaseModel):
73
  """
static/script.js CHANGED
@@ -43,6 +43,18 @@ function switchMode(mode) {
43
  hideResults();
44
  }
45
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  // Keyboard shortcuts management
47
  function setupKeyboardHandlers() {
48
  document.getElementById('doc-id').addEventListener('keypress', function(e) {
@@ -71,12 +83,16 @@ async function searchSingle() {
71
  updateHeaderStats('Searching...');
72
 
73
  try {
 
 
 
 
74
  const response = await fetch(`/find/single`, {
75
  method: 'POST',
76
  headers: {
77
  'Content-Type': 'application/json',
78
  },
79
- body: JSON.stringify({ doc_id: docId })
80
  });
81
 
82
  const data = await response.json();
@@ -118,12 +134,16 @@ async function searchBatch() {
118
  updateHeaderStats('Searching...');
119
 
120
  try {
 
 
 
 
121
  const response = await fetch(`/find/batch`, {
122
  method: 'POST',
123
  headers: {
124
  'Content-Type': 'application/json',
125
  },
126
- body: JSON.stringify({ doc_ids: docIds })
127
  });
128
 
129
  const data = await response.json();
 
43
  hideResults();
44
  }
45
 
46
+ // Show version field only when the input looks like a spec (ETSI or 3GPP)
47
+ const ETSI_SPEC_RE = /^\d{3} \d{3}/;
48
+ const GPP_SPEC_RE = /^\d{2}\.\d{3}/;
49
+
50
+ function toggleVersionField() {
51
+ const docId = document.getElementById('doc-id').value.trim();
52
+ const group = document.getElementById('single-version-group');
53
+ const isSpec = ETSI_SPEC_RE.test(docId) || GPP_SPEC_RE.test(docId);
54
+ group.style.display = isSpec ? 'block' : 'none';
55
+ if (!isSpec) document.getElementById('doc-version').value = '';
56
+ }
57
+
58
  // Keyboard shortcuts management
59
  function setupKeyboardHandlers() {
60
  document.getElementById('doc-id').addEventListener('keypress', function(e) {
 
83
  updateHeaderStats('Searching...');
84
 
85
  try {
86
+ const version = document.getElementById('doc-version').value.trim() || null;
87
+ const body = { doc_id: docId };
88
+ if (version) body.version = version;
89
+
90
  const response = await fetch(`/find/single`, {
91
  method: 'POST',
92
  headers: {
93
  'Content-Type': 'application/json',
94
  },
95
+ body: JSON.stringify(body)
96
  });
97
 
98
  const data = await response.json();
 
134
  updateHeaderStats('Searching...');
135
 
136
  try {
137
+ const version = document.getElementById('batch-version').value.trim() || null;
138
+ const body = { doc_ids: docIds };
139
+ if (version) body.version = version;
140
+
141
  const response = await fetch(`/find/batch`, {
142
  method: 'POST',
143
  headers: {
144
  'Content-Type': 'application/json',
145
  },
146
+ body: JSON.stringify(body)
147
  });
148
 
149
  const data = await response.json();
static/style.css CHANGED
@@ -136,6 +136,21 @@ body {
136
  color: var(--text-primary);
137
  }
138
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  .input-group {
140
  display: flex;
141
  gap: 0.75rem;
 
136
  color: var(--text-primary);
137
  }
138
 
139
+ .label-hint {
140
+ font-weight: 400;
141
+ font-size: 0.8rem;
142
+ color: var(--text-secondary);
143
+ }
144
+
145
+ .version-group {
146
+ animation: fadeIn 0.2s ease;
147
+ }
148
+
149
+ @keyframes fadeIn {
150
+ from { opacity: 0; transform: translateY(-4px); }
151
+ to { opacity: 1; transform: translateY(0); }
152
+ }
153
+
154
  .input-group {
155
  display: flex;
156
  gap: 0.75rem;
templates/index.html CHANGED
@@ -35,10 +35,14 @@
35
  <div class="form-group">
36
  <label for="doc-id">Document ID</label>
37
  <div class="input-group">
38
- <input type="text" id="doc-id" placeholder="e.g., 23.401, S1-123456, SET(24)123">
39
  <button class="btn btn-primary" onclick="searchSingle()">Search</button>
40
  </div>
41
  </div>
 
 
 
 
42
  </div>
43
 
44
  <!-- Batch Search -->
@@ -46,8 +50,12 @@
46
  <div class="form-group">
47
  <label for="batch-ids">List of IDs (one per line)</label>
48
  <textarea id="batch-ids" placeholder="23.401&#10;S1-123456&#10;103 666" rows="6"></textarea>
49
- <button class="btn btn-primary" onclick="searchBatch()">Search Batch</button>
50
  </div>
 
 
 
 
 
51
  </div>
52
 
53
  <!-- Keyword Search -->
 
35
  <div class="form-group">
36
  <label for="doc-id">Document ID</label>
37
  <div class="input-group">
38
+ <input type="text" id="doc-id" placeholder="e.g., 23.401, S1-123456, SET(24)123, 102 223" oninput="toggleVersionField()">
39
  <button class="btn btn-primary" onclick="searchSingle()">Search</button>
40
  </div>
41
  </div>
42
+ <div class="form-group version-group" id="single-version-group" style="display:none;">
43
+ <label for="doc-version">Version <span class="label-hint">(leave empty for latest)</span></label>
44
+ <input type="text" id="doc-version" placeholder="e.g., 17.6.0">
45
+ </div>
46
  </div>
47
 
48
  <!-- Batch Search -->
 
50
  <div class="form-group">
51
  <label for="batch-ids">List of IDs (one per line)</label>
52
  <textarea id="batch-ids" placeholder="23.401&#10;S1-123456&#10;103 666" rows="6"></textarea>
 
53
  </div>
54
+ <div class="form-group version-group">
55
+ <label for="batch-version">Version <span class="label-hint">(applies to all specs — leave empty for latest)</span></label>
56
+ <input type="text" id="batch-version" placeholder="e.g., 17.6.0">
57
+ </div>
58
+ <button class="btn btn-primary" onclick="searchBatch()">Search Batch</button>
59
  </div>
60
 
61
  <!-- Keyword Search -->