Skip to content

Commit 21c4d70

Browse files
committed
Replace local imports with URL mappings
1 parent 68270eb commit 21c4d70

File tree

6 files changed

+182
-69
lines changed

6 files changed

+182
-69
lines changed

ogc/bblocks/entrypoint.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
#!/usr/bin/env python3
2+
## http_interceptor needs to be the first import
3+
# to properly monkey-patch urllib and requests
4+
from ogc.bblocks import http_interceptor
25
import datetime
36
import os
47
import shutil
@@ -201,15 +204,18 @@
201204
sparql_conf = bb_config.get('sparql', {}) or {}
202205
if sparql_conf and sparql_conf.get('query'):
203206
register_additional_metadata['sparqlEndpoint'] = sparql_conf['query']
207+
schema_oas30_downcompile = bb_config.get('schema-oas30-downcompile', False)
204208

205209
bb_local_config_file = Path('bblocks-config-local.yml')
206210
if not bb_local_config_file.is_file():
207211
bb_local_config_file = Path('bblocks-config-local.yaml')
208-
import_local_mappings = None
212+
local_url_mappings = None
209213
if bb_local_config_file.is_file():
210214
bb_local_config = load_yaml(filename=bb_local_config_file)
211-
import_local_mappings = bb_local_config.get('imports-local')
212-
schema_oas30_downcompile = bb_config.get('schema-oas30-downcompile', False)
215+
if bb_local_config.get('imports-local'):
216+
raise ValueError('Local imports are deprecated, please use local URL mappings instead: '
217+
'https://ogcincubator.github.io/bblocks-docs/create/imports#local-url-mappings')
218+
local_url_mappings = bb_local_config.get('url-mappings')
213219

214220
register_additional_metadata['modified'] = datetime.datetime.now().isoformat()
215221

@@ -256,24 +262,28 @@
256262

257263
# 1. Postprocess BBs
258264
print(f"Running postprocess...", file=sys.stderr)
259-
postprocess(registered_items_path=items_dir,
260-
output_file=args.register_file,
261-
base_url=base_url,
262-
generated_docs_path=args.generated_docs_path,
263-
templates_dir=templates_dir,
264-
fail_on_error=fail_on_error,
265-
id_prefix=id_prefix,
266-
annotated_path=annotated_path,
267-
test_outputs_path=args.test_outputs_path,
268-
github_base_url=github_base_url,
269-
imported_registers=imported_registers,
270-
bb_filter=args.filter,
271-
steps=steps,
272-
git_repo_path=git_repo_path,
273-
viewer_path=(args.viewer_path or '.') if deploy_viewer else None,
274-
additional_metadata=register_additional_metadata,
275-
import_local_mappings=import_local_mappings,
276-
schemas_oas30_downcompile=schema_oas30_downcompile)
265+
try:
266+
if local_url_mappings:
267+
http_interceptor.enable(local_url_mappings)
268+
postprocess(registered_items_path=items_dir,
269+
output_file=args.register_file,
270+
base_url=base_url,
271+
generated_docs_path=args.generated_docs_path,
272+
templates_dir=templates_dir,
273+
fail_on_error=fail_on_error,
274+
id_prefix=id_prefix,
275+
annotated_path=annotated_path,
276+
test_outputs_path=args.test_outputs_path,
277+
github_base_url=github_base_url,
278+
imported_registers=imported_registers,
279+
bb_filter=args.filter,
280+
steps=steps,
281+
git_repo_path=git_repo_path,
282+
viewer_path=(args.viewer_path or '.') if deploy_viewer else None,
283+
additional_metadata=register_additional_metadata,
284+
schemas_oas30_downcompile=schema_oas30_downcompile)
285+
finally:
286+
http_interceptor.disable()
277287

278288
# 2. Uplift register.json
279289
print(f"Running semantic uplift of {register_file}", file=sys.stderr)

ogc/bblocks/http_interceptor.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import mimetypes
2+
import urllib.request
3+
from email.message import Message
4+
from pathlib import Path
5+
from urllib.request import Request
6+
7+
import requests
8+
from requests import Response
9+
10+
_original_urlopen = urllib.request.urlopen
11+
_original_requests_session_request = requests.Session.request
12+
13+
_url_mappings: dict[str, Path] = {}
14+
_mocked = False
15+
16+
17+
class MockHTTPResponse:
18+
def __init__(self, url, content, status=200, headers: dict[str, str] | None = None):
19+
self.url = url
20+
self._content = content.encode('utf-8') if isinstance(content, str) else content
21+
self._status = status
22+
self.headers = Message()
23+
mime_type = mimetypes.guess_type(url)[0]
24+
25+
if mime_type is not None:
26+
self.headers.add_header('Content-Type', mime_type)
27+
if isinstance(headers, dict):
28+
for h, v in headers.items():
29+
self.headers.add_header(h, v)
30+
31+
def read(self):
32+
return self._content
33+
34+
def getcode(self):
35+
return self._status
36+
37+
def geturl(self):
38+
return self.url
39+
40+
def info(self):
41+
return self.headers
42+
43+
def __enter__(self):
44+
return self
45+
46+
def __exit__(self, *args):
47+
pass
48+
49+
50+
class MockRequestsResponse(Response):
51+
def __init__(self, url, content, status_code=200):
52+
super().__init__()
53+
self.url = url
54+
self.status_code = status_code
55+
self._content = content
56+
mime_type = mimetypes.guess_type(url)[0]
57+
if mime_type is not None:
58+
self.headers['Content-Type'] = mime_type
59+
60+
61+
def load_content(url: str):
62+
url_mapping, local_path = None, None
63+
for um, lp in _url_mappings.items():
64+
if url.startswith(um):
65+
url_mapping = um
66+
local_path = lp
67+
break
68+
if not url_mapping:
69+
return None
70+
71+
rel_path = url[len(url_mapping):]
72+
if rel_path.startswith('/'):
73+
rel_path = rel_path[1:]
74+
local_file = local_path / rel_path
75+
if not local_file.exists():
76+
raise IOError(f'Local file {local_file} for URL {url} from mapping {url_mapping} does not exist')
77+
with open(local_file, 'rb') as f:
78+
return f.read()
79+
80+
81+
def enable(url_mappings: dict[str, str | Path] | None = None):
82+
global _url_mappings, _mocked
83+
if url_mappings is None:
84+
_url_mappings.clear()
85+
else:
86+
_url_mappings = {k: Path(v) for k, v in url_mappings.items()}
87+
88+
if not _mocked:
89+
90+
def mock_urlopen(request: str | Request, *args, **kwargs):
91+
url = request if not isinstance(request, Request) else request.full_url
92+
content = load_content(url)
93+
if content is not None:
94+
return MockHTTPResponse(url=url, content=content)
95+
else:
96+
return _original_urlopen(request, *args, **kwargs)
97+
98+
def mock_requests_session_requests(self, method, url, *args, **kwargs):
99+
content = load_content(url)
100+
if content is not None:
101+
return MockRequestsResponse(url=url, content=content)
102+
else:
103+
return _original_requests_session_request(self, method, url, *args, **kwargs)
104+
105+
urllib.request.urlopen = mock_urlopen
106+
requests.Session.request = mock_requests_session_requests
107+
_mocked = True
108+
109+
110+
def disable():
111+
urllib.request.urlopen = _original_urlopen
112+
requests.Session.request = _original_requests_session_request
113+
114+
115+
# Enable with empty mappings to override elements from the moment we are imported
116+
enable()

ogc/bblocks/models.py

Lines changed: 23 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -331,11 +331,10 @@ def get_files_with_references(self) -> list[PathOrUrl]:
331331

332332
class ImportedBuildingBlocks:
333333

334-
def __init__(self, metadata_urls: list[str] | None, local_mappings: dict[str, str] | None):
334+
def __init__(self, metadata_urls: list[str] | None):
335335
self.bblocks: dict[str, dict] = {}
336336
self.imported_registers: dict[str, list[str]] = {}
337337
self.real_metadata_urls: dict[str, str] = {}
338-
self.local_mappings = local_mappings
339338
if metadata_urls:
340339
pending_urls = deque(metadata_urls)
341340
while pending_urls:
@@ -351,46 +350,22 @@ def load(self, metadata_url: str) -> tuple[list[str], str]:
351350

352351
tested_locations: dict[str | Path, Any] = {}
353352

354-
if self.local_mappings and metadata_url in self.local_mappings:
355-
metadata_url = self.local_mappings[metadata_url]
356-
if metadata_url.startswith('file://'):
357-
metadata_url = metadata_url[len('file://'):]
358-
elif metadata_url.startswith('https://') or metadata_url.startswith('http://'):
359-
raise ValueError(
360-
f"Local import mapping for {metadata_url} points to a remote URL, "
361-
f"which is not supported. Please use a local path instead."
362-
)
363-
metadata_path = Path(self.local_mappings[metadata_url])
364-
for path in (metadata_path, metadata_path / 'build/register.json', metadata_path / 'register.json'):
365-
try:
366-
tested_locations[path] = True
367-
if path.is_file():
368-
print('Using local import mapping for', metadata_url, '(from file', path , ')', file=sys.stderr)
369-
imported = load_yaml(filename=path)
370-
if (isinstance(imported, dict) and 'bblocks' in imported) or isinstance(imported, list):
371-
break
372-
except:
373-
# Ignore exceptions
374-
imported = None
375-
pass
376-
377-
else:
378-
metadata_url_trailing = metadata_url + ('' if metadata_url.endswith('/') else '/')
379-
for url in (metadata_url,
380-
metadata_url_trailing + 'build/register.json',
381-
metadata_url_trailing + 'register.json'):
382-
try:
383-
tested_locations[url] = True
384-
r = requests.get(url)
385-
if r.ok:
386-
imported = r.json()
387-
if (isinstance(imported, dict) and 'bblocks' in imported) or isinstance(imported, list):
388-
metadata_url = url
389-
break
390-
except:
391-
# Ignore exceptions
392-
imported = None
393-
pass
353+
metadata_url_trailing = metadata_url + ('' if metadata_url.endswith('/') else '/')
354+
for url in (metadata_url,
355+
metadata_url_trailing + 'build/register.json',
356+
metadata_url_trailing + 'register.json'):
357+
try:
358+
tested_locations[url] = True
359+
r = requests.get(url)
360+
if r.ok:
361+
imported = r.json()
362+
if (isinstance(imported, dict) and 'bblocks' in imported) or isinstance(imported, list):
363+
metadata_url = url
364+
break
365+
except:
366+
# Ignore exceptions
367+
imported = None
368+
pass
394369

395370
if imported is None:
396371
tested_locations_str = ' ' + '\n '.join(str(loc) for loc in tested_locations.keys())
@@ -498,6 +473,12 @@ def __init__(self,
498473
dep_graph.add_node(bblock.identifier)
499474
dep_graph.add_edges_from([(d, bblock.identifier) for d in bblock.metadata.get('dependsOn', ())])
500475

476+
for a, b in dep_graph.edges:
477+
if a not in self.bblocks and a not in self.imported_bblocks:
478+
raise ValueError(f'Invalid reference to bblock {a}'
479+
f' from {b} - the bblock does not exist'
480+
f' - perhaps an import is missing?')
481+
501482
cycles = list(nx.simple_cycles(dep_graph))
502483
if cycles:
503484
cycles_str = '\n - '.join(' -> '.join(reversed(c)) + ' -> ' + c[-1] for c in cycles)

ogc/bblocks/postprocess.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ def postprocess(registered_items_path: str | Path = 'registereditems',
4242
git_repo_path: Path | None = None,
4343
viewer_path: str | Path | None = None,
4444
additional_metadata: dict | None = None,
45-
import_local_mappings: dict[str, str] | None = None,
4645
schemas_oas30_downcompile=False) -> list[dict]:
4746

4847
cwd = Path().resolve()
@@ -68,7 +67,7 @@ def postprocess(registered_items_path: str | Path = 'registereditems',
6867
registered_items_path = Path(registered_items_path)
6968

7069
child_bblocks = []
71-
imported_bblocks = ImportedBuildingBlocks(imported_registers, local_mappings=import_local_mappings)
70+
imported_bblocks = ImportedBuildingBlocks(imported_registers)
7271
bbr = BuildingBlockRegister(registered_items_path,
7372
fail_on_error=fail_on_error,
7473
prefix=id_prefix,

ogc/bblocks/util.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,3 +299,4 @@ def find_references_xml(contents) -> set[str]:
299299
"""
300300
# TODO: Implement for XML schemas
301301
pass
302+

ogc/bblocks/validation/rdf.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ def validate(self, filename: Path, output_filename: Path, report: ValidationRepo
317317
if graph is False:
318318
return False
319319
if graph is None:
320-
return
320+
return None
321321

322322
if graph is not None and (contents or filename.suffix != '.ttl'):
323323
try:
@@ -349,7 +349,7 @@ def validate(self, filename: Path, output_filename: Path, report: ValidationRepo
349349
'exception': e.__class__.__qualname__,
350350
}
351351
))
352-
return
352+
return None
353353

354354
if graph:
355355
if self.shacl_errors:
@@ -360,9 +360,9 @@ def validate(self, filename: Path, output_filename: Path, report: ValidationRepo
360360
is_error=True,
361361
is_global=True,
362362
))
363-
return
363+
return None
364364
if not self.shacl_graphs:
365-
return
365+
return None
366366

367367
if additional_shacl_closures:
368368
additional_shacl_closures = [c if is_url(c) else self.bblock.files_path.joinpath(c)
@@ -430,6 +430,8 @@ def validate(self, filename: Path, output_filename: Path, report: ValidationRepo
430430
'focusNodes': focus_nodes_payload,
431431
}
432432
))
433+
return None
434+
return None
433435
except ParseBaseException as e:
434436
if e.args:
435437
query_lines = e.args[0].splitlines()
@@ -450,6 +452,7 @@ def validate(self, filename: Path, output_filename: Path, report: ValidationRepo
450452
'shaclFile': str(shacl_file),
451453
}
452454
))
455+
return None
453456
except ReportableRuntimeError as e:
454457
report.add_entry(ValidationReportEntry(
455458
section=ValidationReportSection.SHACL,
@@ -462,3 +465,6 @@ def validate(self, filename: Path, output_filename: Path, report: ValidationRepo
462465
'shaclFile': str(shacl_file),
463466
}
464467
))
468+
return None
469+
return None
470+
return None

0 commit comments

Comments
 (0)