oapi-profile-builder 2.5.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.5.0/src/oapi_profile_builder.egg-info → oapi_profile_builder-3.0.0}/PKG-INFO +55 -15
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/README.md +54 -14
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/pyproject.toml +1 -1
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/generate.py +25 -20
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/models.py +112 -73
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0/src/oapi_profile_builder.egg-info}/PKG-INFO +55 -15
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/LICENSE +0 -0
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/setup.cfg +0 -0
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/__init__.py +0 -0
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/cite.py +0 -0
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/cite_features.py +0 -0
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/cli.py +0 -0
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/compile.py +0 -0
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/server_validation.py +0 -0
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder.egg-info/SOURCES.txt +0 -0
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder.egg-info/dependency_links.txt +0 -0
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder.egg-info/entry_points.txt +0 -0
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder.egg-info/requires.txt +0 -0
- {oapi_profile_builder-2.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder.egg-info/top_level.txt +0 -0
{oapi_profile_builder-2.5.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.5.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.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/models.py
RENAMED
|
@@ -164,20 +164,87 @@ class SubscriptionFilter(BaseModel):
|
|
|
164
164
|
type: FilterType = FilterType.string
|
|
165
165
|
|
|
166
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
|
+
|
|
167
190
|
class ExtentRequirements(BaseModel):
|
|
168
|
-
"""Profile-level extent restrictions per OGC API - EDR Part 3.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
)
|
|
176
241
|
|
|
177
242
|
@model_validator(mode="after")
|
|
178
243
|
def validate_crs_specification(self) -> ExtentRequirements:
|
|
179
|
-
if self.
|
|
180
|
-
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
|
+
)
|
|
181
248
|
return self
|
|
182
249
|
|
|
183
250
|
|
|
@@ -395,69 +462,49 @@ class ServiceProfile(BaseModel):
|
|
|
395
462
|
|
|
396
463
|
@model_validator(mode="after")
|
|
397
464
|
def validate_collection_extent_patterns(self) -> ServiceProfile:
|
|
398
|
-
"""Validate collection CRS/TRS/VRS values against extent_requirements
|
|
465
|
+
"""Validate collection CRS/TRS/VRS values against extent_requirements constraints."""
|
|
399
466
|
if not self.extent_requirements:
|
|
400
467
|
return self
|
|
401
468
|
er = self.extent_requirements
|
|
402
469
|
|
|
403
|
-
#
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
+
)
|
|
407
484
|
|
|
408
485
|
for coll in self.collections:
|
|
409
|
-
# ---
|
|
486
|
+
# --- extent.spatial.crs → extent_crs ---
|
|
410
487
|
crs = coll.extent.spatial.crs if coll.extent and coll.extent.spatial else None
|
|
411
488
|
if crs:
|
|
412
|
-
|
|
413
|
-
raise ValueError(
|
|
414
|
-
f"Collection '{coll.id}' CRS '{crs}' is not in allowed_crs "
|
|
415
|
-
f"{er.allowed_crs}"
|
|
416
|
-
)
|
|
417
|
-
if crs_pat and not crs_pat.fullmatch(crs):
|
|
418
|
-
raise ValueError(
|
|
419
|
-
f"Collection '{coll.id}' CRS '{crs}' does not match "
|
|
420
|
-
f"crs_pattern '{er.crs_pattern}'"
|
|
421
|
-
)
|
|
489
|
+
_check(f"Collection '{coll.id}' extent.spatial.crs", crs, er.extent_crs)
|
|
422
490
|
|
|
423
|
-
# ---
|
|
424
|
-
trs =
|
|
425
|
-
coll.extent.temporal.trs
|
|
426
|
-
if coll.extent and coll.extent.temporal else None
|
|
427
|
-
)
|
|
491
|
+
# --- extent.temporal.trs → extent_trs ---
|
|
492
|
+
trs = coll.extent.temporal.trs if coll.extent and coll.extent.temporal else None
|
|
428
493
|
if trs:
|
|
429
|
-
|
|
430
|
-
raise ValueError(
|
|
431
|
-
f"Collection '{coll.id}' TRS '{trs}' is not in allowed_trs "
|
|
432
|
-
f"{er.allowed_trs}"
|
|
433
|
-
)
|
|
434
|
-
if trs_pat and not trs_pat.fullmatch(trs):
|
|
435
|
-
raise ValueError(
|
|
436
|
-
f"Collection '{coll.id}' TRS '{trs}' does not match "
|
|
437
|
-
f"trs_pattern '{er.trs_pattern}'"
|
|
438
|
-
)
|
|
494
|
+
_check(f"Collection '{coll.id}' extent.temporal.trs", trs, er.extent_trs)
|
|
439
495
|
|
|
440
|
-
# ---
|
|
441
|
-
vrs =
|
|
442
|
-
coll.extent.vertical.vrs
|
|
443
|
-
if coll.extent and coll.extent.vertical else None
|
|
444
|
-
)
|
|
496
|
+
# --- extent.vertical.vrs → extent_vrs ---
|
|
497
|
+
vrs = coll.extent.vertical.vrs if coll.extent and coll.extent.vertical else None
|
|
445
498
|
if vrs:
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
raise ValueError(
|
|
453
|
-
f"Collection '{coll.id}' VRS '{vrs}' does not match "
|
|
454
|
-
f"vrs_pattern '{er.vrs_pattern}'"
|
|
455
|
-
)
|
|
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)
|
|
456
505
|
|
|
457
|
-
# --- crs_details in data_queries ---
|
|
458
|
-
|
|
459
|
-
# Validate those CRS values against the same allowed_crs / crs_pattern.
|
|
460
|
-
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:
|
|
461
508
|
for qt_name, qt_val in coll.data_queries:
|
|
462
509
|
if qt_val is None:
|
|
463
510
|
continue
|
|
@@ -468,19 +515,11 @@ class ServiceProfile(BaseModel):
|
|
|
468
515
|
)
|
|
469
516
|
for entry in (crs_details or []):
|
|
470
517
|
dq_crs = entry.get("crs") if isinstance(entry, dict) else None
|
|
471
|
-
if
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
f"crs_details CRS '{dq_crs}' is not in allowed_crs "
|
|
477
|
-
f"{er.allowed_crs}"
|
|
478
|
-
)
|
|
479
|
-
if crs_pat and not crs_pat.fullmatch(dq_crs):
|
|
480
|
-
raise ValueError(
|
|
481
|
-
f"Collection '{coll.id}' data query '{qt_name}' "
|
|
482
|
-
f"crs_details CRS '{dq_crs}' does not match "
|
|
483
|
-
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,
|
|
484
523
|
)
|
|
485
524
|
return self
|
|
486
525
|
|
{oapi_profile_builder-2.5.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.5.0 → oapi_profile_builder-3.0.0}/src/oapi_profile_builder/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-2.5.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.5.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
|