oapi-profile-builder 2.4.0__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {oapi_profile_builder-2.4.0/src/oapi_profile_builder.egg-info → oapi_profile_builder-3.0.0}/PKG-INFO +55 -15
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/README.md +54 -14
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/pyproject.toml +1 -1
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/generate.py +25 -20
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/models.py +134 -82
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0/src/oapi_profile_builder.egg-info}/PKG-INFO +55 -15
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/LICENSE +0 -0
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/setup.cfg +0 -0
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/__init__.py +0 -0
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/cite.py +0 -0
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/cite_features.py +0 -0
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/cli.py +0 -0
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/compile.py +0 -0
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/server_validation.py +0 -0
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder.egg-info/SOURCES.txt +0 -0
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder.egg-info/dependency_links.txt +0 -0
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder.egg-info/entry_points.txt +0 -0
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder.egg-info/requires.txt +0 -0
- {oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder.egg-info/top_level.txt +0 -0
{oapi_profile_builder-2.4.0/src/oapi_profile_builder.egg-info → oapi_profile_builder-3.0.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oapi-profile-builder
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Authoritative tooling for creating OGC API Service Profiles (EDR, Features)
|
|
5
5
|
Author-email: Shane Mill <shane.mill@noaa.gov>
|
|
6
6
|
License: Apache License
|
|
@@ -234,18 +234,27 @@ keywords:
|
|
|
234
234
|
- my-parameter
|
|
235
235
|
- my-domain
|
|
236
236
|
|
|
237
|
+
# ── Profile-level constraints ─────────────────────────────────────────────
|
|
238
|
+
# Define what any conforming implementation MUST support.
|
|
239
|
+
# Validated at build time and embedded in the generated OpenAPI as
|
|
240
|
+
# enum/pattern constraints so runtime tools can enforce them.
|
|
237
241
|
required_conformance_classes:
|
|
238
242
|
- "http://www.opengis.net/spec/ogcapi-edr-1/1.0/conf/core"
|
|
239
243
|
|
|
240
244
|
extent_requirements:
|
|
241
245
|
minimum_bbox: [-180, -90, 180, 90]
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
extent_crs:
|
|
247
|
+
allowed:
|
|
248
|
+
- "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
|
244
249
|
|
|
245
250
|
output_formats:
|
|
246
251
|
- name: GeoJSON
|
|
247
252
|
media_type: application/geo+json
|
|
248
253
|
|
|
254
|
+
# ── Reference metadata ────────────────────────────────────────────────────
|
|
255
|
+
# Describes the structure of collections a conforming service should expose.
|
|
256
|
+
# For dynamic fields (extent intervals, custom dimension values), use the
|
|
257
|
+
# full possible range — not a snapshot of current live data.
|
|
249
258
|
collections:
|
|
250
259
|
- id: my_collection
|
|
251
260
|
links:
|
|
@@ -264,6 +273,11 @@ collections:
|
|
|
264
273
|
unit:
|
|
265
274
|
label: Celsius
|
|
266
275
|
symbol: "°C"
|
|
276
|
+
|
|
277
|
+
# ── Normative requirements and abstract tests ─────────────────────────────
|
|
278
|
+
# Drive the AsciiDoc/PDF output. Tests must reference a valid requirement id.
|
|
279
|
+
requirements: []
|
|
280
|
+
abstract_tests: []
|
|
267
281
|
```
|
|
268
282
|
|
|
269
283
|
### Top-level fields
|
|
@@ -297,6 +311,16 @@ collections:
|
|
|
297
311
|
|
|
298
312
|
Uses the [edr-pydantic](https://github.com/KNMI/edr-pydantic) `Collection` model — the same schema an EDR server returns at `/collections/{id}`.
|
|
299
313
|
|
|
314
|
+
> **Service-level vs collection-level fields**
|
|
315
|
+
>
|
|
316
|
+
> The profile has two distinct levels that serve different purposes — they don't merge or override each other:
|
|
317
|
+
>
|
|
318
|
+
> - **Service-level fields** (`extent_requirements`, `parameter_schema`, `parameter_name_pattern`, `collection_id_pattern`) define *constraints* that any conforming implementation must satisfy. They are validated at build time and embedded in the generated OpenAPI as schema constraints (enums, patterns) so runtime tools can enforce them against a live server.
|
|
319
|
+
>
|
|
320
|
+
> - **`collections[]`** provides *reference metadata* — the structure and representative values of collections a conforming service should expose. This mirrors what an EDR server returns at `/collections/{id}` and drives the generated OpenAPI paths and response schemas.
|
|
321
|
+
>
|
|
322
|
+
> For fields that are inherently dynamic (e.g. `extent.temporal.interval`, `extent.custom[].values`), use representative values that describe the *full possible range*, not a snapshot of current live data. For example, use an open-ended temporal interval `["2020-01-01T00:00:00Z", null]` rather than a specific end date, and list all valid custom dimension values rather than only those currently in the dataset. The `validate-server` command tests API structure and response schema conformance — it does not assert that specific data values in responses match the profile config exactly.
|
|
323
|
+
|
|
300
324
|
| Field | Required | Description |
|
|
301
325
|
|---|---|---|
|
|
302
326
|
| `id` | yes | Collection identifier |
|
|
@@ -363,25 +387,41 @@ parameter_names:
|
|
|
363
387
|
|
|
364
388
|
### `extent_requirements`
|
|
365
389
|
|
|
366
|
-
Constrains CRS, TRS, and VRS values across all collections. Validated at build time and embedded in the generated OpenAPI.
|
|
390
|
+
Constrains CRS, TRS, and VRS values across all collections. Validated at build time and embedded in the generated OpenAPI as `enum` or `pattern` constraints on the collection response schema.
|
|
391
|
+
|
|
392
|
+
These are **profile-level constraints** — they define what any conforming implementation must support, independent of the specific values a live service happens to have at a given moment.
|
|
393
|
+
|
|
394
|
+
CRS/TRS/VRS constraints are split into two distinct concerns:
|
|
395
|
+
|
|
396
|
+
- `extent_crs` / `extent_trs` / `extent_vrs` — constrain the **single value** used to express the extent (`extent.spatial.crs`, `extent.temporal.trs`, `extent.vertical.vrs`). Typically just CRS84 / Gregorian.
|
|
397
|
+
- `supported_crs` / `supported_trs` / `supported_vrs` — constrain the **list of values** the service supports for queries (the top-level `crs` array and `data_queries.*.variables.crs_details`). Usually broader than the extent constraint.
|
|
398
|
+
|
|
399
|
+
Each constraint uses either `allowed` (exact list) or `pattern` (regex) — not both.
|
|
367
400
|
|
|
368
401
|
```yaml
|
|
369
402
|
extent_requirements:
|
|
370
403
|
minimum_bbox: [-180, -90, 180, 90]
|
|
371
404
|
|
|
372
|
-
#
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
#
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
405
|
+
# Extent is always expressed in CRS84
|
|
406
|
+
extent_crs:
|
|
407
|
+
allowed:
|
|
408
|
+
- "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
|
409
|
+
|
|
410
|
+
# Service supports CRS84 plus any EPSG CRS for queries
|
|
411
|
+
supported_crs:
|
|
412
|
+
pattern: "^(http://www\\.opengis\\.net/def/crs/OGC/1\\.3/CRS84|http://www\\.opengis\\.net/def/crs/EPSG/.*)$"
|
|
413
|
+
# or an explicit list:
|
|
414
|
+
# supported_crs:
|
|
415
|
+
# allowed:
|
|
416
|
+
# - "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
|
417
|
+
# - "http://www.opengis.net/def/crs/EPSG/0/4326"
|
|
418
|
+
|
|
419
|
+
extent_trs:
|
|
420
|
+
allowed:
|
|
421
|
+
- "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian"
|
|
382
422
|
```
|
|
383
423
|
|
|
384
|
-
|
|
424
|
+
At least one of `extent_crs` or `supported_crs` is required. The same applies to TRS and VRS — all are optional individually but at least one CRS constraint must be present.
|
|
385
425
|
|
|
386
426
|
---
|
|
387
427
|
|
|
@@ -189,18 +189,27 @@ keywords:
|
|
|
189
189
|
- my-parameter
|
|
190
190
|
- my-domain
|
|
191
191
|
|
|
192
|
+
# ── Profile-level constraints ─────────────────────────────────────────────
|
|
193
|
+
# Define what any conforming implementation MUST support.
|
|
194
|
+
# Validated at build time and embedded in the generated OpenAPI as
|
|
195
|
+
# enum/pattern constraints so runtime tools can enforce them.
|
|
192
196
|
required_conformance_classes:
|
|
193
197
|
- "http://www.opengis.net/spec/ogcapi-edr-1/1.0/conf/core"
|
|
194
198
|
|
|
195
199
|
extent_requirements:
|
|
196
200
|
minimum_bbox: [-180, -90, 180, 90]
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
extent_crs:
|
|
202
|
+
allowed:
|
|
203
|
+
- "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
|
199
204
|
|
|
200
205
|
output_formats:
|
|
201
206
|
- name: GeoJSON
|
|
202
207
|
media_type: application/geo+json
|
|
203
208
|
|
|
209
|
+
# ── Reference metadata ────────────────────────────────────────────────────
|
|
210
|
+
# Describes the structure of collections a conforming service should expose.
|
|
211
|
+
# For dynamic fields (extent intervals, custom dimension values), use the
|
|
212
|
+
# full possible range — not a snapshot of current live data.
|
|
204
213
|
collections:
|
|
205
214
|
- id: my_collection
|
|
206
215
|
links:
|
|
@@ -219,6 +228,11 @@ collections:
|
|
|
219
228
|
unit:
|
|
220
229
|
label: Celsius
|
|
221
230
|
symbol: "°C"
|
|
231
|
+
|
|
232
|
+
# ── Normative requirements and abstract tests ─────────────────────────────
|
|
233
|
+
# Drive the AsciiDoc/PDF output. Tests must reference a valid requirement id.
|
|
234
|
+
requirements: []
|
|
235
|
+
abstract_tests: []
|
|
222
236
|
```
|
|
223
237
|
|
|
224
238
|
### Top-level fields
|
|
@@ -252,6 +266,16 @@ collections:
|
|
|
252
266
|
|
|
253
267
|
Uses the [edr-pydantic](https://github.com/KNMI/edr-pydantic) `Collection` model — the same schema an EDR server returns at `/collections/{id}`.
|
|
254
268
|
|
|
269
|
+
> **Service-level vs collection-level fields**
|
|
270
|
+
>
|
|
271
|
+
> The profile has two distinct levels that serve different purposes — they don't merge or override each other:
|
|
272
|
+
>
|
|
273
|
+
> - **Service-level fields** (`extent_requirements`, `parameter_schema`, `parameter_name_pattern`, `collection_id_pattern`) define *constraints* that any conforming implementation must satisfy. They are validated at build time and embedded in the generated OpenAPI as schema constraints (enums, patterns) so runtime tools can enforce them against a live server.
|
|
274
|
+
>
|
|
275
|
+
> - **`collections[]`** provides *reference metadata* — the structure and representative values of collections a conforming service should expose. This mirrors what an EDR server returns at `/collections/{id}` and drives the generated OpenAPI paths and response schemas.
|
|
276
|
+
>
|
|
277
|
+
> For fields that are inherently dynamic (e.g. `extent.temporal.interval`, `extent.custom[].values`), use representative values that describe the *full possible range*, not a snapshot of current live data. For example, use an open-ended temporal interval `["2020-01-01T00:00:00Z", null]` rather than a specific end date, and list all valid custom dimension values rather than only those currently in the dataset. The `validate-server` command tests API structure and response schema conformance — it does not assert that specific data values in responses match the profile config exactly.
|
|
278
|
+
|
|
255
279
|
| Field | Required | Description |
|
|
256
280
|
|---|---|---|
|
|
257
281
|
| `id` | yes | Collection identifier |
|
|
@@ -318,25 +342,41 @@ parameter_names:
|
|
|
318
342
|
|
|
319
343
|
### `extent_requirements`
|
|
320
344
|
|
|
321
|
-
Constrains CRS, TRS, and VRS values across all collections. Validated at build time and embedded in the generated OpenAPI.
|
|
345
|
+
Constrains CRS, TRS, and VRS values across all collections. Validated at build time and embedded in the generated OpenAPI as `enum` or `pattern` constraints on the collection response schema.
|
|
346
|
+
|
|
347
|
+
These are **profile-level constraints** — they define what any conforming implementation must support, independent of the specific values a live service happens to have at a given moment.
|
|
348
|
+
|
|
349
|
+
CRS/TRS/VRS constraints are split into two distinct concerns:
|
|
350
|
+
|
|
351
|
+
- `extent_crs` / `extent_trs` / `extent_vrs` — constrain the **single value** used to express the extent (`extent.spatial.crs`, `extent.temporal.trs`, `extent.vertical.vrs`). Typically just CRS84 / Gregorian.
|
|
352
|
+
- `supported_crs` / `supported_trs` / `supported_vrs` — constrain the **list of values** the service supports for queries (the top-level `crs` array and `data_queries.*.variables.crs_details`). Usually broader than the extent constraint.
|
|
353
|
+
|
|
354
|
+
Each constraint uses either `allowed` (exact list) or `pattern` (regex) — not both.
|
|
322
355
|
|
|
323
356
|
```yaml
|
|
324
357
|
extent_requirements:
|
|
325
358
|
minimum_bbox: [-180, -90, 180, 90]
|
|
326
359
|
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
#
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
360
|
+
# Extent is always expressed in CRS84
|
|
361
|
+
extent_crs:
|
|
362
|
+
allowed:
|
|
363
|
+
- "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
|
364
|
+
|
|
365
|
+
# Service supports CRS84 plus any EPSG CRS for queries
|
|
366
|
+
supported_crs:
|
|
367
|
+
pattern: "^(http://www\\.opengis\\.net/def/crs/OGC/1\\.3/CRS84|http://www\\.opengis\\.net/def/crs/EPSG/.*)$"
|
|
368
|
+
# or an explicit list:
|
|
369
|
+
# supported_crs:
|
|
370
|
+
# allowed:
|
|
371
|
+
# - "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
|
372
|
+
# - "http://www.opengis.net/def/crs/EPSG/0/4326"
|
|
373
|
+
|
|
374
|
+
extent_trs:
|
|
375
|
+
allowed:
|
|
376
|
+
- "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian"
|
|
337
377
|
```
|
|
338
378
|
|
|
339
|
-
|
|
379
|
+
At least one of `extent_crs` or `supported_crs` is required. The same applies to TRS and VRS — all are optional individually but at least one CRS constraint must be present.
|
|
340
380
|
|
|
341
381
|
---
|
|
342
382
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "oapi-profile-builder"
|
|
7
|
-
version = "
|
|
7
|
+
version = "3.0.0"
|
|
8
8
|
description = "Authoritative tooling for creating OGC API Service Profiles (EDR, Features)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
{oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/generate.py
RENAMED
|
@@ -416,24 +416,26 @@ def _collection_response_schema(coll: Collection,
|
|
|
416
416
|
- Full parameter_names objects with all fields
|
|
417
417
|
- parameter_name_pattern as propertyNames constraint
|
|
418
418
|
"""
|
|
419
|
-
crs_schema: dict = {"type": "string"}
|
|
420
|
-
trs_schema: dict = {"type": "string"}
|
|
421
|
-
vrs_schema: dict = {"type": "string"}
|
|
419
|
+
crs_schema: dict = {"type": "string"} # extent.spatial.crs
|
|
420
|
+
trs_schema: dict = {"type": "string"} # extent.temporal.trs
|
|
421
|
+
vrs_schema: dict = {"type": "string"} # extent.vertical.vrs
|
|
422
|
+
supported_crs_schema: dict = {"type": "string"} # crs[] and crs_details[].crs
|
|
422
423
|
|
|
423
424
|
if profile and profile.extent_requirements:
|
|
424
425
|
er = profile.extent_requirements
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
426
|
+
|
|
427
|
+
def _apply(schema: dict, constraint: "object | None") -> None:
|
|
428
|
+
if constraint is None:
|
|
429
|
+
return
|
|
430
|
+
if getattr(constraint, "allowed", None):
|
|
431
|
+
schema["enum"] = constraint.allowed
|
|
432
|
+
elif getattr(constraint, "pattern", None):
|
|
433
|
+
schema["pattern"] = constraint.pattern
|
|
434
|
+
|
|
435
|
+
_apply(crs_schema, er.extent_crs)
|
|
436
|
+
_apply(trs_schema, er.extent_trs)
|
|
437
|
+
_apply(vrs_schema, er.extent_vrs)
|
|
438
|
+
_apply(supported_crs_schema, er.supported_crs)
|
|
437
439
|
|
|
438
440
|
# Build parameter_names schema — use profile-supplied schema if present,
|
|
439
441
|
# otherwise fall back to the default EDR Parameter schema.
|
|
@@ -459,11 +461,14 @@ def _collection_response_schema(coll: Collection,
|
|
|
459
461
|
if coll.output_formats:
|
|
460
462
|
output_formats_schema["items"] = {"type": "string", "enum": list(coll.output_formats)}
|
|
461
463
|
|
|
462
|
-
# Build crs array schema — use collection-level crs list if present
|
|
463
|
-
|
|
464
|
+
# Build crs array schema — use collection-level crs list if present,
|
|
465
|
+
# otherwise apply supported_crs constraint from extent_requirements.
|
|
466
|
+
# supported_crs is distinct from extent_crs: it constrains what the
|
|
467
|
+
# service supports for queries, not how the extent itself is expressed.
|
|
464
468
|
if coll.crs:
|
|
465
|
-
|
|
466
|
-
|
|
469
|
+
crs_array_schema: dict = {"type": "array", "items": {"type": "string", "enum": list(coll.crs)}}
|
|
470
|
+
else:
|
|
471
|
+
crs_array_schema = {"type": "array", "items": supported_crs_schema}
|
|
467
472
|
|
|
468
473
|
schema: dict = {
|
|
469
474
|
"type": "object",
|
|
@@ -585,7 +590,7 @@ def _collection_response_schema(coll: Collection,
|
|
|
585
590
|
"type": "object",
|
|
586
591
|
"required": ["crs"],
|
|
587
592
|
"properties": {
|
|
588
|
-
"crs":
|
|
593
|
+
"crs": supported_crs_schema,
|
|
589
594
|
"wkt": {"type": "string"},
|
|
590
595
|
},
|
|
591
596
|
},
|
{oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/models.py
RENAMED
|
@@ -60,6 +60,7 @@ from pydantic import AwareDatetime, BaseModel, Field, field_validator, model_val
|
|
|
60
60
|
# ---------------------------------------------------------------------------
|
|
61
61
|
|
|
62
62
|
from edr_pydantic.base_model import EdrBaseModel # noqa: E402 — after stdlib imports
|
|
63
|
+
from edr_pydantic.parameter import Parameters # noqa: E402
|
|
63
64
|
|
|
64
65
|
|
|
65
66
|
class TemporalWithNullBounds(EdrBaseModel):
|
|
@@ -80,18 +81,32 @@ class ExtentWithNullBounds(EDRExtent):
|
|
|
80
81
|
|
|
81
82
|
class Collection(EDRCollection):
|
|
82
83
|
"""
|
|
83
|
-
EDR Collection with a null-aware temporal extent.
|
|
84
|
+
EDR Collection with a null-aware temporal extent and optional parameter_names.
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
extent uses ExtentWithNullBounds so open-ended intervals like
|
|
87
|
-
["2020-01-01T00:00:00Z", null] are accepted.
|
|
86
|
+
Differences from edr_pydantic.collections.Collection:
|
|
88
87
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
- extent uses ExtentWithNullBounds so open-ended intervals like
|
|
89
|
+
["2020-01-01T00:00:00Z", null] are accepted (spec-compliant).
|
|
90
|
+
|
|
91
|
+
- parameter_names is Optional (default None). edr-pydantic marks it as
|
|
92
|
+
required, but OGC API - EDR Part 3 allows a profile to define the
|
|
93
|
+
*schema* for parameter objects via parameter_schema without mandating
|
|
94
|
+
any specific parameter names in the profile document itself.
|
|
95
|
+
|
|
96
|
+
- post_queries controls whether POST operations are generated for this
|
|
97
|
+
collection's data query endpoints. None means inherit the profile-level
|
|
98
|
+
allow_post_queries default. Set to True or False to override per-collection.
|
|
92
99
|
"""
|
|
93
100
|
|
|
94
101
|
extent: Optional[ExtentWithNullBounds] = None # type: ignore[assignment]
|
|
102
|
+
parameter_names: Optional[Parameters] = Field( # type: ignore[assignment]
|
|
103
|
+
default=None,
|
|
104
|
+
description=(
|
|
105
|
+
"Map of parameter id → Parameter object. "
|
|
106
|
+
"Optional — omit when using parameter_schema to describe the structure "
|
|
107
|
+
"without mandating specific parameter names."
|
|
108
|
+
),
|
|
109
|
+
)
|
|
95
110
|
post_queries: Optional[bool] = Field(
|
|
96
111
|
default=None,
|
|
97
112
|
description=(
|
|
@@ -149,20 +164,87 @@ class SubscriptionFilter(BaseModel):
|
|
|
149
164
|
type: FilterType = FilterType.string
|
|
150
165
|
|
|
151
166
|
|
|
167
|
+
class CrsConstraint(BaseModel):
|
|
168
|
+
"""An enum or regex constraint on a CRS/TRS/VRS value or list.
|
|
169
|
+
|
|
170
|
+
Exactly one of `allowed` or `pattern` must be set.
|
|
171
|
+
"""
|
|
172
|
+
allowed: list[str] | None = Field(
|
|
173
|
+
default=None,
|
|
174
|
+
description="Enumerated list of accepted values",
|
|
175
|
+
)
|
|
176
|
+
pattern: str | None = Field(
|
|
177
|
+
default=None,
|
|
178
|
+
description="Regular expression that accepted values must match",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
@model_validator(mode="after")
|
|
182
|
+
def exactly_one(self) -> CrsConstraint:
|
|
183
|
+
if self.allowed is None and self.pattern is None:
|
|
184
|
+
raise ValueError("Either 'allowed' or 'pattern' must be specified")
|
|
185
|
+
if self.allowed is not None and self.pattern is not None:
|
|
186
|
+
raise ValueError("Only one of 'allowed' or 'pattern' may be specified, not both")
|
|
187
|
+
return self
|
|
188
|
+
|
|
189
|
+
|
|
152
190
|
class ExtentRequirements(BaseModel):
|
|
153
|
-
"""Profile-level extent restrictions per OGC API - EDR Part 3.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
191
|
+
"""Profile-level extent restrictions per OGC API - EDR Part 3.
|
|
192
|
+
|
|
193
|
+
CRS/TRS/VRS constraints are split into two distinct concerns:
|
|
194
|
+
|
|
195
|
+
- ``extent_crs`` / ``extent_trs`` / ``extent_vrs`` — constrain the single
|
|
196
|
+
CRS/TRS/VRS value used to *express* the extent (``extent.spatial.crs``,
|
|
197
|
+
``extent.temporal.trs``, ``extent.vertical.vrs``). Typically this is
|
|
198
|
+
just CRS84 / Gregorian.
|
|
199
|
+
|
|
200
|
+
- ``supported_crs`` / ``supported_trs`` / ``supported_vrs`` — constrain
|
|
201
|
+
the list of CRS/TRS/VRS values the service *supports* for queries
|
|
202
|
+
(the top-level ``crs`` array and ``data_queries.*.variables.crs_details``).
|
|
203
|
+
This is usually broader than the extent CRS.
|
|
204
|
+
"""
|
|
205
|
+
minimum_bbox: list[float] = Field(
|
|
206
|
+
min_length=4, max_length=4,
|
|
207
|
+
description="Minimum spatial bounds [minLon, minLat, maxLon, maxLat]",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# --- Extent CRS/TRS/VRS (single value on extent object) ---
|
|
211
|
+
extent_crs: CrsConstraint | None = Field(
|
|
212
|
+
default=None,
|
|
213
|
+
description="Constraint on extent.spatial.crs — the CRS the extent is expressed in",
|
|
214
|
+
)
|
|
215
|
+
extent_trs: CrsConstraint | None = Field(
|
|
216
|
+
default=None,
|
|
217
|
+
description="Constraint on extent.temporal.trs — the TRS the temporal extent is expressed in",
|
|
218
|
+
)
|
|
219
|
+
extent_vrs: CrsConstraint | None = Field(
|
|
220
|
+
default=None,
|
|
221
|
+
description="Constraint on extent.vertical.vrs — the VRS the vertical extent is expressed in",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# --- Supported CRS/TRS/VRS (lists — what the service supports for queries) ---
|
|
225
|
+
supported_crs: CrsConstraint | None = Field(
|
|
226
|
+
default=None,
|
|
227
|
+
description=(
|
|
228
|
+
"Constraint on the top-level crs array and crs_details — "
|
|
229
|
+
"the CRS values the service supports for data queries. "
|
|
230
|
+
"Typically broader than extent_crs."
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
supported_trs: CrsConstraint | None = Field(
|
|
234
|
+
default=None,
|
|
235
|
+
description="Constraint on supported TRS values for data queries",
|
|
236
|
+
)
|
|
237
|
+
supported_vrs: CrsConstraint | None = Field(
|
|
238
|
+
default=None,
|
|
239
|
+
description="Constraint on supported VRS values for data queries",
|
|
240
|
+
)
|
|
161
241
|
|
|
162
242
|
@model_validator(mode="after")
|
|
163
243
|
def validate_crs_specification(self) -> ExtentRequirements:
|
|
164
|
-
if self.
|
|
165
|
-
raise ValueError(
|
|
244
|
+
if self.extent_crs is None and self.supported_crs is None:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
"At least one of 'extent_crs' or 'supported_crs' must be specified"
|
|
247
|
+
)
|
|
166
248
|
return self
|
|
167
249
|
|
|
168
250
|
|
|
@@ -327,13 +409,11 @@ class ServiceProfile(BaseModel):
|
|
|
327
409
|
if not coll.parameter_names:
|
|
328
410
|
continue
|
|
329
411
|
for param_name, param in coll.parameter_names.root.items():
|
|
330
|
-
# Check if unit is specified
|
|
331
412
|
if not hasattr(param, 'unit') or param.unit is None:
|
|
332
413
|
raise ValueError(
|
|
333
414
|
f"Parameter '{param_name}' in collection '{coll.id}' must specify unit "
|
|
334
415
|
f"(required by OGC API - EDR Part 3 REQ_parameter-names)"
|
|
335
416
|
)
|
|
336
|
-
# Check if observedProperty is specified (already required by edr-pydantic)
|
|
337
417
|
if not hasattr(param, 'observedProperty') or param.observedProperty is None:
|
|
338
418
|
raise ValueError(
|
|
339
419
|
f"Parameter '{param_name}' in collection '{coll.id}' must specify observedProperty"
|
|
@@ -382,69 +462,49 @@ class ServiceProfile(BaseModel):
|
|
|
382
462
|
|
|
383
463
|
@model_validator(mode="after")
|
|
384
464
|
def validate_collection_extent_patterns(self) -> ServiceProfile:
|
|
385
|
-
"""Validate collection CRS/TRS/VRS values against extent_requirements
|
|
465
|
+
"""Validate collection CRS/TRS/VRS values against extent_requirements constraints."""
|
|
386
466
|
if not self.extent_requirements:
|
|
387
467
|
return self
|
|
388
468
|
er = self.extent_requirements
|
|
389
469
|
|
|
390
|
-
#
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
470
|
+
# Helper: check a single value against a CrsConstraint
|
|
471
|
+
def _check(label: str, value: str, constraint: "CrsConstraint | None") -> None:
|
|
472
|
+
if constraint is None:
|
|
473
|
+
return
|
|
474
|
+
if constraint.allowed is not None and value not in constraint.allowed:
|
|
475
|
+
raise ValueError(
|
|
476
|
+
f"{label} '{value}' is not in allowed list {constraint.allowed}"
|
|
477
|
+
)
|
|
478
|
+
if constraint.pattern is not None:
|
|
479
|
+
pat = _compile_optional_pattern(label, constraint.pattern)
|
|
480
|
+
if pat and not pat.fullmatch(value):
|
|
481
|
+
raise ValueError(
|
|
482
|
+
f"{label} '{value}' does not match pattern '{constraint.pattern}'"
|
|
483
|
+
)
|
|
394
484
|
|
|
395
485
|
for coll in self.collections:
|
|
396
|
-
# ---
|
|
486
|
+
# --- extent.spatial.crs → extent_crs ---
|
|
397
487
|
crs = coll.extent.spatial.crs if coll.extent and coll.extent.spatial else None
|
|
398
488
|
if crs:
|
|
399
|
-
|
|
400
|
-
raise ValueError(
|
|
401
|
-
f"Collection '{coll.id}' CRS '{crs}' is not in allowed_crs "
|
|
402
|
-
f"{er.allowed_crs}"
|
|
403
|
-
)
|
|
404
|
-
if crs_pat and not crs_pat.fullmatch(crs):
|
|
405
|
-
raise ValueError(
|
|
406
|
-
f"Collection '{coll.id}' CRS '{crs}' does not match "
|
|
407
|
-
f"crs_pattern '{er.crs_pattern}'"
|
|
408
|
-
)
|
|
489
|
+
_check(f"Collection '{coll.id}' extent.spatial.crs", crs, er.extent_crs)
|
|
409
490
|
|
|
410
|
-
# ---
|
|
411
|
-
trs =
|
|
412
|
-
coll.extent.temporal.trs
|
|
413
|
-
if coll.extent and coll.extent.temporal else None
|
|
414
|
-
)
|
|
491
|
+
# --- extent.temporal.trs → extent_trs ---
|
|
492
|
+
trs = coll.extent.temporal.trs if coll.extent and coll.extent.temporal else None
|
|
415
493
|
if trs:
|
|
416
|
-
|
|
417
|
-
raise ValueError(
|
|
418
|
-
f"Collection '{coll.id}' TRS '{trs}' is not in allowed_trs "
|
|
419
|
-
f"{er.allowed_trs}"
|
|
420
|
-
)
|
|
421
|
-
if trs_pat and not trs_pat.fullmatch(trs):
|
|
422
|
-
raise ValueError(
|
|
423
|
-
f"Collection '{coll.id}' TRS '{trs}' does not match "
|
|
424
|
-
f"trs_pattern '{er.trs_pattern}'"
|
|
425
|
-
)
|
|
494
|
+
_check(f"Collection '{coll.id}' extent.temporal.trs", trs, er.extent_trs)
|
|
426
495
|
|
|
427
|
-
# ---
|
|
428
|
-
vrs =
|
|
429
|
-
coll.extent.vertical.vrs
|
|
430
|
-
if coll.extent and coll.extent.vertical else None
|
|
431
|
-
)
|
|
496
|
+
# --- extent.vertical.vrs → extent_vrs ---
|
|
497
|
+
vrs = coll.extent.vertical.vrs if coll.extent and coll.extent.vertical else None
|
|
432
498
|
if vrs:
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
raise ValueError(
|
|
440
|
-
f"Collection '{coll.id}' VRS '{vrs}' does not match "
|
|
441
|
-
f"vrs_pattern '{er.vrs_pattern}'"
|
|
442
|
-
)
|
|
499
|
+
_check(f"Collection '{coll.id}' extent.vertical.vrs", vrs, er.extent_vrs)
|
|
500
|
+
|
|
501
|
+
# --- top-level crs array → supported_crs ---
|
|
502
|
+
if coll.crs and er.supported_crs:
|
|
503
|
+
for crs_val in coll.crs:
|
|
504
|
+
_check(f"Collection '{coll.id}' crs[]", crs_val, er.supported_crs)
|
|
443
505
|
|
|
444
|
-
# --- crs_details in data_queries ---
|
|
445
|
-
|
|
446
|
-
# Validate those CRS values against the same allowed_crs / crs_pattern.
|
|
447
|
-
if coll.data_queries and (er.allowed_crs or crs_pat):
|
|
506
|
+
# --- crs_details in data_queries → supported_crs ---
|
|
507
|
+
if coll.data_queries and er.supported_crs:
|
|
448
508
|
for qt_name, qt_val in coll.data_queries:
|
|
449
509
|
if qt_val is None:
|
|
450
510
|
continue
|
|
@@ -455,19 +515,11 @@ class ServiceProfile(BaseModel):
|
|
|
455
515
|
)
|
|
456
516
|
for entry in (crs_details or []):
|
|
457
517
|
dq_crs = entry.get("crs") if isinstance(entry, dict) else None
|
|
458
|
-
if
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
f"crs_details CRS '{dq_crs}' is not in allowed_crs "
|
|
464
|
-
f"{er.allowed_crs}"
|
|
465
|
-
)
|
|
466
|
-
if crs_pat and not crs_pat.fullmatch(dq_crs):
|
|
467
|
-
raise ValueError(
|
|
468
|
-
f"Collection '{coll.id}' data query '{qt_name}' "
|
|
469
|
-
f"crs_details CRS '{dq_crs}' does not match "
|
|
470
|
-
f"crs_pattern '{er.crs_pattern}'"
|
|
518
|
+
if dq_crs:
|
|
519
|
+
_check(
|
|
520
|
+
f"Collection '{coll.id}' data_queries.{qt_name}.crs_details",
|
|
521
|
+
dq_crs,
|
|
522
|
+
er.supported_crs,
|
|
471
523
|
)
|
|
472
524
|
return self
|
|
473
525
|
|
{oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0/src/oapi_profile_builder.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oapi-profile-builder
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.0
|
|
4
4
|
Summary: Authoritative tooling for creating OGC API Service Profiles (EDR, Features)
|
|
5
5
|
Author-email: Shane Mill <shane.mill@noaa.gov>
|
|
6
6
|
License: Apache License
|
|
@@ -234,18 +234,27 @@ keywords:
|
|
|
234
234
|
- my-parameter
|
|
235
235
|
- my-domain
|
|
236
236
|
|
|
237
|
+
# ── Profile-level constraints ─────────────────────────────────────────────
|
|
238
|
+
# Define what any conforming implementation MUST support.
|
|
239
|
+
# Validated at build time and embedded in the generated OpenAPI as
|
|
240
|
+
# enum/pattern constraints so runtime tools can enforce them.
|
|
237
241
|
required_conformance_classes:
|
|
238
242
|
- "http://www.opengis.net/spec/ogcapi-edr-1/1.0/conf/core"
|
|
239
243
|
|
|
240
244
|
extent_requirements:
|
|
241
245
|
minimum_bbox: [-180, -90, 180, 90]
|
|
242
|
-
|
|
243
|
-
|
|
246
|
+
extent_crs:
|
|
247
|
+
allowed:
|
|
248
|
+
- "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
|
244
249
|
|
|
245
250
|
output_formats:
|
|
246
251
|
- name: GeoJSON
|
|
247
252
|
media_type: application/geo+json
|
|
248
253
|
|
|
254
|
+
# ── Reference metadata ────────────────────────────────────────────────────
|
|
255
|
+
# Describes the structure of collections a conforming service should expose.
|
|
256
|
+
# For dynamic fields (extent intervals, custom dimension values), use the
|
|
257
|
+
# full possible range — not a snapshot of current live data.
|
|
249
258
|
collections:
|
|
250
259
|
- id: my_collection
|
|
251
260
|
links:
|
|
@@ -264,6 +273,11 @@ collections:
|
|
|
264
273
|
unit:
|
|
265
274
|
label: Celsius
|
|
266
275
|
symbol: "°C"
|
|
276
|
+
|
|
277
|
+
# ── Normative requirements and abstract tests ─────────────────────────────
|
|
278
|
+
# Drive the AsciiDoc/PDF output. Tests must reference a valid requirement id.
|
|
279
|
+
requirements: []
|
|
280
|
+
abstract_tests: []
|
|
267
281
|
```
|
|
268
282
|
|
|
269
283
|
### Top-level fields
|
|
@@ -297,6 +311,16 @@ collections:
|
|
|
297
311
|
|
|
298
312
|
Uses the [edr-pydantic](https://github.com/KNMI/edr-pydantic) `Collection` model — the same schema an EDR server returns at `/collections/{id}`.
|
|
299
313
|
|
|
314
|
+
> **Service-level vs collection-level fields**
|
|
315
|
+
>
|
|
316
|
+
> The profile has two distinct levels that serve different purposes — they don't merge or override each other:
|
|
317
|
+
>
|
|
318
|
+
> - **Service-level fields** (`extent_requirements`, `parameter_schema`, `parameter_name_pattern`, `collection_id_pattern`) define *constraints* that any conforming implementation must satisfy. They are validated at build time and embedded in the generated OpenAPI as schema constraints (enums, patterns) so runtime tools can enforce them against a live server.
|
|
319
|
+
>
|
|
320
|
+
> - **`collections[]`** provides *reference metadata* — the structure and representative values of collections a conforming service should expose. This mirrors what an EDR server returns at `/collections/{id}` and drives the generated OpenAPI paths and response schemas.
|
|
321
|
+
>
|
|
322
|
+
> For fields that are inherently dynamic (e.g. `extent.temporal.interval`, `extent.custom[].values`), use representative values that describe the *full possible range*, not a snapshot of current live data. For example, use an open-ended temporal interval `["2020-01-01T00:00:00Z", null]` rather than a specific end date, and list all valid custom dimension values rather than only those currently in the dataset. The `validate-server` command tests API structure and response schema conformance — it does not assert that specific data values in responses match the profile config exactly.
|
|
323
|
+
|
|
300
324
|
| Field | Required | Description |
|
|
301
325
|
|---|---|---|
|
|
302
326
|
| `id` | yes | Collection identifier |
|
|
@@ -363,25 +387,41 @@ parameter_names:
|
|
|
363
387
|
|
|
364
388
|
### `extent_requirements`
|
|
365
389
|
|
|
366
|
-
Constrains CRS, TRS, and VRS values across all collections. Validated at build time and embedded in the generated OpenAPI.
|
|
390
|
+
Constrains CRS, TRS, and VRS values across all collections. Validated at build time and embedded in the generated OpenAPI as `enum` or `pattern` constraints on the collection response schema.
|
|
391
|
+
|
|
392
|
+
These are **profile-level constraints** — they define what any conforming implementation must support, independent of the specific values a live service happens to have at a given moment.
|
|
393
|
+
|
|
394
|
+
CRS/TRS/VRS constraints are split into two distinct concerns:
|
|
395
|
+
|
|
396
|
+
- `extent_crs` / `extent_trs` / `extent_vrs` — constrain the **single value** used to express the extent (`extent.spatial.crs`, `extent.temporal.trs`, `extent.vertical.vrs`). Typically just CRS84 / Gregorian.
|
|
397
|
+
- `supported_crs` / `supported_trs` / `supported_vrs` — constrain the **list of values** the service supports for queries (the top-level `crs` array and `data_queries.*.variables.crs_details`). Usually broader than the extent constraint.
|
|
398
|
+
|
|
399
|
+
Each constraint uses either `allowed` (exact list) or `pattern` (regex) — not both.
|
|
367
400
|
|
|
368
401
|
```yaml
|
|
369
402
|
extent_requirements:
|
|
370
403
|
minimum_bbox: [-180, -90, 180, 90]
|
|
371
404
|
|
|
372
|
-
#
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
#
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
405
|
+
# Extent is always expressed in CRS84
|
|
406
|
+
extent_crs:
|
|
407
|
+
allowed:
|
|
408
|
+
- "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
|
409
|
+
|
|
410
|
+
# Service supports CRS84 plus any EPSG CRS for queries
|
|
411
|
+
supported_crs:
|
|
412
|
+
pattern: "^(http://www\\.opengis\\.net/def/crs/OGC/1\\.3/CRS84|http://www\\.opengis\\.net/def/crs/EPSG/.*)$"
|
|
413
|
+
# or an explicit list:
|
|
414
|
+
# supported_crs:
|
|
415
|
+
# allowed:
|
|
416
|
+
# - "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
|
417
|
+
# - "http://www.opengis.net/def/crs/EPSG/0/4326"
|
|
418
|
+
|
|
419
|
+
extent_trs:
|
|
420
|
+
allowed:
|
|
421
|
+
- "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian"
|
|
382
422
|
```
|
|
383
423
|
|
|
384
|
-
|
|
424
|
+
At least one of `extent_crs` or `supported_crs` is required. The same applies to TRS and VRS — all are optional individually but at least one CRS constraint must be present.
|
|
385
425
|
|
|
386
426
|
---
|
|
387
427
|
|
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/cite_features.py
RENAMED
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-2.4.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/compile.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|