Skip to content

Commit 103410f

Browse files
committed
build: Upgrade ocdsextensionregistry
1 parent ad9b2c6 commit 103410f

File tree

4 files changed

+10
-313
lines changed

4 files changed

+10
-313
lines changed

common-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ myst-parser==0.18.1
9090
# via -r common-requirements.in
9191
ocds-babel==0.3.6
9292
# via -r common-requirements.in
93-
ocdsextensionregistry==0.5.0
93+
ocdsextensionregistry==0.6.0
9494
# via -r common-requirements.in
9595
ocdsindex==0.2.0
9696
# via -r common-requirements.in

manage.py

Lines changed: 6 additions & 310 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import warnings
1010
from collections import defaultdict
1111
from contextlib import contextmanager
12-
from copy import deepcopy
1312
from glob import glob
1413
from io import StringIO
1514
from pathlib import Path
@@ -23,6 +22,7 @@
2322
from babel.messages.pofile import read_po
2423
from docutils.utils import relative_path
2524
from lxml import etree
25+
from ocdsextensionregistry import get_versioned_release_schema
2626
from ocdskit.schema import get_schema_fields
2727

2828
basedir = Path(__file__).resolve().parent
@@ -40,77 +40,6 @@ def custom_warning_formatter(message, category, filename, lineno, line=None):
4040

4141
warnings.formatwarning = custom_warning_formatter
4242

43-
versioned_template = json.loads("""
44-
{
45-
"type": "array",
46-
"items": {
47-
"type": "object",
48-
"properties": {
49-
"releaseDate": {
50-
"format": "date-time",
51-
"type": "string"
52-
},
53-
"releaseID": {
54-
"type": "string"
55-
},
56-
"value": {},
57-
"releaseTag": {
58-
"type": "array",
59-
"items": {
60-
"type": "string"
61-
}
62-
}
63-
}
64-
}
65-
}
66-
""")
67-
68-
common_versioned_definitions = {
69-
"StringNullUriVersioned": {
70-
"type": ["string", "null"],
71-
"format": "uri",
72-
},
73-
"StringNullDateTimeVersioned": {
74-
"type": ["string", "null"],
75-
"format": "date-time",
76-
},
77-
"StringNullVersioned": {
78-
"type": ["string", "null"],
79-
"format": None,
80-
},
81-
}
82-
83-
recognized_types = (
84-
# Array
85-
["array"],
86-
["array", "null"], # optional string arrays
87-
# Object
88-
["object"],
89-
["object", "null"], # /Organization/details
90-
# String
91-
["string"],
92-
["string", "null"],
93-
# Literal
94-
["boolean", "null"],
95-
["integer", "null"],
96-
["number", "null"],
97-
# Mixed
98-
["string", "integer"],
99-
["string", "integer", "null"],
100-
)
101-
102-
keywords_to_remove = (
103-
# Metadata keywords
104-
# https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-6
105-
"title",
106-
"description",
107-
"default",
108-
# Extended keywords
109-
# http://os4d.opendataservices.coop/development/schema/#extended-json-schema
110-
"omitWhenMerged",
111-
"wholeListMerge",
112-
)
113-
11443

11544
def json_load(filename, library=json, **kwargs):
11645
"""Load JSON data from the given filename."""
@@ -149,249 +78,13 @@ def get(url):
14978
return response
15079

15180

152-
def coerce_to_list(data, key):
153-
"""Return the value of the ``key`` key in the ``data`` mapping. If the value is a string, wrap it in an array."""
154-
item = data.get(key, [])
155-
if isinstance(item, str):
156-
return [item]
157-
return item
158-
159-
16081
def get_metaschema():
16182
"""Patches and returns the JSON Schema Draft 4 metaschema."""
16283
return json_merge_patch.merge(
16384
json_load("metaschema/json-schema-draft-4.json"), json_load("metaschema/meta-schema-patch.json")
16485
)
16586

16687

167-
def get_common_definition_ref(item):
168-
"""
169-
Return a schema that references the common definition that the ``item`` matches: "StringNullUriVersioned",
170-
"StringNullDateTimeVersioned" or "StringNullVersioned".
171-
"""
172-
for name, keywords in common_versioned_definitions.items():
173-
# If the item matches the definition.
174-
if any(item.get(keyword) != value for keyword, value in keywords.items()):
175-
continue
176-
# And adds no keywords to the definition.
177-
if any(keyword not in {*keywords, *keywords_to_remove} for keyword in item):
178-
continue
179-
return {"$ref": f"#/definitions/{name}"}
180-
return None
181-
182-
183-
def add_versioned(schema, unversioned_pointers, pointer=""):
184-
"""Call ``_add_versioned`` on each field."""
185-
for key, value in schema["properties"].items():
186-
new_pointer = f"{pointer}/properties/{key}"
187-
_add_versioned(schema, unversioned_pointers, new_pointer, key, value)
188-
189-
for key, value in schema.get("definitions", {}).items():
190-
new_pointer = f"{pointer}/definitions/{key}"
191-
add_versioned(value, unversioned_pointers, pointer=new_pointer)
192-
193-
194-
def _add_versioned(schema, unversioned_pointers, pointer, key, value):
195-
"""
196-
Perform the changes to the schema to refer to versioned/unversioned definitions.
197-
198-
:param schema dict: the schema of the object on which the field is defined
199-
:param unversioned_pointers set: JSON Pointers to ``id`` fields to leave unversioned if the object is in an array
200-
:param pointer str: the field's pointer
201-
:param key str: the field's name
202-
:param value str: the field's schema
203-
"""
204-
# Skip unversioned fields.
205-
if pointer in unversioned_pointers:
206-
return
207-
208-
types = coerce_to_list(value, "type")
209-
210-
# If a type is unrecognized, we might need to update this script.
211-
if (
212-
"$ref" not in value
213-
and types not in recognized_types
214-
and not (pointer == "/definitions/Quantity/properties/value" and types == ["string", "number", "null"])
215-
):
216-
warnings.warn(f"{pointer} has unrecognized type {types}")
217-
218-
# For example, if $ref is used.
219-
if not types:
220-
# Ignore the `amendment` field, which had no `id` field in OCDS 1.0.
221-
if "deprecated" not in value:
222-
versioned_pointer = f"{value['$ref'][1:]}/properties/id"
223-
# If the `id` field is on an object not in an array, it needs to be versioned (e.g. buyer/properties/id).
224-
if versioned_pointer in unversioned_pointers:
225-
value["$ref"] = value["$ref"] + "VersionedId"
226-
return
227-
228-
# Reference a common versioned definition if possible, to limit the size of the schema.
229-
ref = get_common_definition_ref(value)
230-
if ref:
231-
schema["properties"][key] = ref
232-
233-
# Iterate into objects with properties like `Item.unit`. Otherwise, version objects with no properties as a
234-
# whole, like `Organization.details`.
235-
elif types == ["object"] and "properties" in value:
236-
add_versioned(value, unversioned_pointers, pointer=pointer)
237-
238-
else:
239-
new_value = deepcopy(value)
240-
241-
if types == ["array"]:
242-
item_types = coerce_to_list(value["items"], "type")
243-
244-
# See https://standard.open-contracting.org/latest/en/schema/merging/#whole-list-merge
245-
if value.get("wholeListMerge"):
246-
# Update `$ref` to the unversioned definition.
247-
if "$ref" in value["items"]:
248-
new_value["items"]["$ref"] = value["items"]["$ref"] + "Unversioned"
249-
# Otherwise, similarly, don't iterate over item properties.
250-
# See https://standard.open-contracting.org/latest/en/schema/merging/#lists
251-
elif "$ref" in value["items"]:
252-
# Leave `$ref` to the versioned definition.
253-
return
254-
# Exceptional case for deprecated `Amendment.changes`.
255-
elif item_types == ["object"] and pointer == "/definitions/Amendment/properties/changes":
256-
return
257-
# Warn in case new combinations are added to the release schema.
258-
elif item_types != ["string"]:
259-
# Note: Versioning the properties of un-$ref'erenced objects in arrays isn't implemented. However,
260-
# this combination hasn't occurred, with the exception of `Amendment/changes`.
261-
warnings.warn(f"{pointer}/items has unexpected type {item_types}")
262-
263-
versioned = deepcopy(versioned_template)
264-
versioned["items"]["properties"]["value"] = new_value
265-
schema["properties"][key] = versioned
266-
267-
268-
def update_refs_to_unversioned_definitions(schema):
269-
"""Replace ``$ref`` values with unversioned definitions."""
270-
for key, value in schema.items():
271-
if key == "$ref":
272-
schema[key] = value + "Unversioned"
273-
elif isinstance(value, dict):
274-
update_refs_to_unversioned_definitions(value)
275-
276-
277-
def get_unversioned_pointers(schema, fields, pointer=""):
278-
"""Return the JSON Pointers to ``id`` fields that must not be versioned if the object is in an array."""
279-
if isinstance(schema, list):
280-
for index, item in enumerate(schema):
281-
get_unversioned_pointers(item, fields, pointer=f"{pointer}/{index}")
282-
elif isinstance(schema, dict):
283-
# Follows the logic of _get_merge_rules in merge.py from ocds-merge.
284-
types = coerce_to_list(schema, "type")
285-
286-
# If an array is whole list merge, its items are unversioned.
287-
if "array" in types and schema.get("wholeListMerge"):
288-
return
289-
if "array" in types and "items" in schema:
290-
item_types = coerce_to_list(schema["items"], "type")
291-
# If an array mixes objects and non-objects, it is whole list merge.
292-
if any(item_type != "object" for item_type in item_types):
293-
return
294-
# If it is an array of objects, any `id` fields are unversioned.
295-
if "id" in schema["items"]["properties"]:
296-
if hasattr(schema["items"], "__reference__"):
297-
reference = schema["items"].__reference__["$ref"][1:]
298-
else:
299-
reference = pointer
300-
fields.add(f"{reference}/properties/id")
301-
302-
for key, value in schema.items():
303-
get_unversioned_pointers(value, fields, pointer=f"{pointer}/{key}")
304-
305-
306-
def remove_omit_when_merged(schema):
307-
"""Remove properties that set ``omitWhenMerged``."""
308-
if isinstance(schema, list):
309-
for item in schema:
310-
remove_omit_when_merged(item)
311-
elif isinstance(schema, dict):
312-
for key, value in schema.items():
313-
if key == "properties":
314-
for prop in list(value):
315-
if value[prop].get("omitWhenMerged"):
316-
del value[prop]
317-
if prop in schema["required"]:
318-
schema["required"].remove(prop)
319-
remove_omit_when_merged(value)
320-
321-
322-
def remove_metadata_and_extended_keywords(schema):
323-
"""Remove metadata and extended keywords from properties and definitions."""
324-
if isinstance(schema, list):
325-
for item in schema:
326-
remove_metadata_and_extended_keywords(item)
327-
elif isinstance(schema, dict):
328-
for key, value in schema.items():
329-
if key in {"definitions", "properties"}:
330-
for subschema in value.values():
331-
for keyword in keywords_to_remove:
332-
subschema.pop(keyword, None)
333-
remove_metadata_and_extended_keywords(value)
334-
335-
336-
def get_versioned_release_schema(schema):
337-
"""Return the versioned release schema."""
338-
# Update schema metadata.
339-
release_with_underscores = release.replace(".", "__")
340-
schema["id"] = (
341-
f"https://standard.open-contracting.org/schema/{release_with_underscores}/versioned-release-validation-schema.json"
342-
)
343-
schema["title"] = "Schema for a compiled, versioned Open Contracting Release."
344-
345-
# Release IDs, dates and tags appear alongside values in the versioned release schema.
346-
remove_omit_when_merged(schema)
347-
348-
# Create unversioned copies of all definitions.
349-
unversioned_definitions = {k + "Unversioned": deepcopy(v) for k, v in schema["definitions"].items()}
350-
update_refs_to_unversioned_definitions(unversioned_definitions)
351-
352-
# Determine which `id` fields occur on objects in arrays.
353-
unversioned_pointers = set()
354-
get_unversioned_pointers(jsonref.replace_refs(schema), unversioned_pointers)
355-
356-
# Omit `ocid` from versioning.
357-
ocid = schema["properties"].pop("ocid")
358-
add_versioned(schema, unversioned_pointers)
359-
schema["properties"]["ocid"] = ocid
360-
361-
# Add the common versioned definitions.
362-
for name, keywords in common_versioned_definitions.items():
363-
versioned = deepcopy(versioned_template)
364-
for keyword, value in keywords.items():
365-
if value:
366-
versioned["items"]["properties"]["value"][keyword] = value
367-
schema["definitions"][name] = versioned
368-
369-
# Add missing definitions.
370-
while True:
371-
try:
372-
jsonref.replace_refs(schema, lazy_load=False)
373-
break
374-
except jsonref.JsonRefError as e:
375-
name = e.cause.args[0]
376-
377-
if name.endswith("VersionedId"):
378-
# Add a copy of an definition with a versioned `id` field, using the same logic as before.
379-
definition = deepcopy(schema["definitions"][name[:-11]])
380-
pointer = f"/definitions/{name[:-11]}/properties/id"
381-
pointers = unversioned_pointers - {pointer}
382-
_add_versioned(definition, pointers, pointer, "id", definition["properties"]["id"])
383-
else:
384-
# Add a copy of an definition with no versioned fields.
385-
definition = unversioned_definitions[name]
386-
387-
schema["definitions"][name] = definition
388-
389-
# Remove all metadata and extended keywords.
390-
remove_metadata_and_extended_keywords(schema)
391-
392-
return schema
393-
394-
39588
@click.group()
39689
def cli():
39790
pass
@@ -523,7 +216,7 @@ def pre_commit():
523216
for field in get_schema_fields(jsonref_release_schema):
524217
name = field.path_components[-1]
525218
# Skip definitions (output dereferenced properties only). Skip deprecated fields.
526-
if field.definition_pointer_components or field.deprecated:
219+
if field.definition or field.deprecated:
527220
continue
528221
multilingual = (
529222
# If a field can be a non-string, it is not multilingual.
@@ -566,7 +259,10 @@ def pre_commit():
566259

567260
json_dump("meta-schema.json", get_metaschema())
568261
json_dump("dereferenced-release-schema.json", jsonref_release_schema)
569-
json_dump("versioned-release-validation-schema.json", get_versioned_release_schema(release_schema))
262+
json_dump(
263+
"versioned-release-validation-schema.json",
264+
get_versioned_release_schema(release_schema, release.replace(".", "__")),
265+
)
570266

571267

572268
@cli.command()

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
ocdskit==1.1.3
66
sphinx-design==0.4.1
77
sphinxcontrib-opencontracting==0.0.8
8-
sphinxcontrib-opendataservices-jsonschema==0.6.1
8+
sphinxcontrib-opendataservices-jsonschema==0.7.1
99
sphinxcontrib-opendataservices==0.5.0

tests/test_schema_integrity.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
import sys
88

99
import jsonref
10+
from ocdsextensionregistry import get_versioned_release_schema
1011

1112
sys.path.append(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
1213

13-
from manage import get_metaschema, get_versioned_release_schema
14+
from manage import get_metaschema
1415

1516

1617
def test_versioned_release_schema_is_in_sync():

0 commit comments

Comments
 (0)