oapi-profile-builder 2.0.2__tar.gz → 2.0.3__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.0.2/src/oapi_profile_builder.egg-info → oapi_profile_builder-2.0.3}/PKG-INFO +163 -6
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/README.md +162 -5
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/pyproject.toml +1 -1
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/generate.py +102 -3
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/models.py +115 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3/src/oapi_profile_builder.egg-info}/PKG-INFO +163 -6
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/LICENSE +0 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/setup.cfg +0 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/__init__.py +0 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/cite.py +0 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/cite_features.py +0 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/cli.py +0 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/compile.py +0 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/server_validation.py +0 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder.egg-info/SOURCES.txt +0 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder.egg-info/dependency_links.txt +0 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder.egg-info/entry_points.txt +0 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder.egg-info/requires.txt +0 -0
- {oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder.egg-info/top_level.txt +0 -0
{oapi_profile_builder-2.0.2/src/oapi_profile_builder.egg-info → oapi_profile_builder-2.0.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oapi-profile-builder
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
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
|
|
@@ -63,6 +63,9 @@ pip install oapi-profile-builder
|
|
|
63
63
|
|
|
64
64
|
## Workflow
|
|
65
65
|
|
|
66
|
+
<img width="1001" height="721" alt="OGC API Service Profile Builder - Pydantic Validation Architecture drawio" src="https://github.com/user-attachments/assets/092c3dfc-549e-41b0-8a92-af0b89689950" />
|
|
67
|
+
|
|
68
|
+
|
|
66
69
|
### 1. Author a Profile Config
|
|
67
70
|
|
|
68
71
|
A profile config is a YAML or JSON file. Start with the minimal example:
|
|
@@ -293,6 +296,133 @@ The skipped tests are optional features not implemented by the server.
|
|
|
293
296
|
|
|
294
297
|
---
|
|
295
298
|
|
|
299
|
+
## Profile Configuration Guide
|
|
300
|
+
|
|
301
|
+
This section explains what is and isn't allowed when creating a profile, and how the tool validates your configuration.
|
|
302
|
+
|
|
303
|
+
### What Gets Validated
|
|
304
|
+
|
|
305
|
+
When you run `generate` or `validate`, the tool instantiates a `ServiceProfile` Pydantic model that enforces all of the following rules before any files are written. If any rule is violated, you get a clear error message pointing to the offending field.
|
|
306
|
+
|
|
307
|
+
#### Profile-Level Fields
|
|
308
|
+
|
|
309
|
+
| Field | Rules |
|
|
310
|
+
|---|---|
|
|
311
|
+
| `name` | Must match `^[a-z0-9_]+$` — lowercase letters, digits, and underscores only. Used in OGC URIs. |
|
|
312
|
+
| `title` | Any non-empty string. |
|
|
313
|
+
| `version` | Any string. Defaults to `"1.0"`. |
|
|
314
|
+
| `collections` | At least one collection is required. No duplicate `id` values. |
|
|
315
|
+
|
|
316
|
+
#### Collection IDs
|
|
317
|
+
|
|
318
|
+
By default, collection IDs can be any string accepted by edr-pydantic. To enforce a naming convention across all collections, use `collection_id_pattern`:
|
|
319
|
+
|
|
320
|
+
```yaml
|
|
321
|
+
# Only allow lowercase snake_case collection IDs
|
|
322
|
+
collection_id_pattern: "^[a-z][a-z0-9_]*$"
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The pattern is matched using Python's `re.fullmatch()`, so it must match the **entire** ID string.
|
|
326
|
+
|
|
327
|
+
#### CRS, TRS, and VRS Constraints
|
|
328
|
+
|
|
329
|
+
Each collection declares a CRS in `extent.spatial.crs`, and optionally a TRS in `extent.temporal.trs` and VRS in `extent.vertical.vrs`. The profile can constrain these values in two ways:
|
|
330
|
+
|
|
331
|
+
**Enumerated list** — only the exact values listed are accepted:
|
|
332
|
+
|
|
333
|
+
```yaml
|
|
334
|
+
extent_requirements:
|
|
335
|
+
minimum_bbox: [-180, -90, 180, 90]
|
|
336
|
+
allowed_crs:
|
|
337
|
+
- "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
|
338
|
+
- "http://www.opengis.net/def/crs/EPSG/0/4326"
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Regex pattern** — any value matching the pattern is accepted:
|
|
342
|
+
|
|
343
|
+
```yaml
|
|
344
|
+
extent_requirements:
|
|
345
|
+
minimum_bbox: [-180, -90, 180, 90]
|
|
346
|
+
# Accept any OGC or EPSG CRS
|
|
347
|
+
crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
If both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both**. At least one of `allowed_crs` or `crs_pattern` is required when `extent_requirements` is present.
|
|
351
|
+
|
|
352
|
+
The same enum/regex approach works for TRS (`allowed_trs` / `trs_pattern`) and VRS (`allowed_vrs` / `vrs_pattern`).
|
|
353
|
+
|
|
354
|
+
#### Parameter Name Constraints
|
|
355
|
+
|
|
356
|
+
By default, parameter names (the keys in `parameter_names`) can be any string. To enforce a naming convention, use `parameter_name_pattern`:
|
|
357
|
+
|
|
358
|
+
```yaml
|
|
359
|
+
# CF-style lowercase parameter names
|
|
360
|
+
parameter_name_pattern: "^[a-z][a-z0-9_]*$"
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```yaml
|
|
364
|
+
# Allow uppercase abbreviations like WMO codes
|
|
365
|
+
parameter_name_pattern: "^[A-Za-z][A-Za-z0-9_]*$"
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Every key in every collection's `parameter_names` must match this pattern. The pattern uses `re.fullmatch()`.
|
|
369
|
+
|
|
370
|
+
Additionally, per OGC API - EDR Part 3, every parameter must specify both `unit` and `observedProperty`. The tool enforces this automatically.
|
|
371
|
+
|
|
372
|
+
#### Requirement and Test IDs
|
|
373
|
+
|
|
374
|
+
| Field | Rules |
|
|
375
|
+
|---|---|
|
|
376
|
+
| Requirement `id` | Must match `^[a-z0-9][a-z0-9\-]*$` — lowercase, digits, hyphens. Cannot end with a hyphen. |
|
|
377
|
+
| AbstractTest `id` | Must exactly equal its `requirement_id`. |
|
|
378
|
+
| AbstractTest `requirement_id` | Must reference an existing requirement `id`. |
|
|
379
|
+
|
|
380
|
+
#### What Happens When Validation Fails
|
|
381
|
+
|
|
382
|
+
The tool prints a Pydantic validation error with the field path and a human-readable message. For example:
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
Value error, Collection 'my_data' CRS 'urn:ogc:def:crs:EPSG::4326'
|
|
386
|
+
does not match crs_pattern '^http://www\.opengis\.net/def/crs/(OGC|EPSG)/.*$'
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
Value error, Parameter name 'WIND_SPEED' in collection 'weather'
|
|
391
|
+
does not match parameter_name_pattern '^[a-z][a-z0-9_]*$'
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
Value error, Collection id 'My-Collection' does not match
|
|
396
|
+
collection_id_pattern '^[a-z][a-z0-9_]*$'
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### How Patterns Flow Into the Generated OpenAPI
|
|
400
|
+
|
|
401
|
+
When you specify `crs_pattern`, `allowed_crs`, or `parameter_name_pattern`, those constraints are embedded in the generated `openapi.yaml` so that runtime validation tools can enforce them:
|
|
402
|
+
|
|
403
|
+
- `crs_pattern` → `pattern` on the CRS string schema in collection responses
|
|
404
|
+
- `allowed_crs` → `enum` on the CRS string schema
|
|
405
|
+
- `trs_pattern` / `allowed_trs` → `pattern` / `enum` on the TRS field in extent.temporal
|
|
406
|
+
- `vrs_pattern` / `allowed_vrs` → `pattern` / `enum` on the VRS field in extent.vertical
|
|
407
|
+
- `parameter_name_pattern` → `propertyNames.pattern` on the `parameter_names` object schema
|
|
408
|
+
|
|
409
|
+
This means schemathesis, CITE tests, and client SDKs can validate server responses against these constraints without needing access to the original profile YAML.
|
|
410
|
+
|
|
411
|
+
### Quick Reference: Regex Examples
|
|
412
|
+
|
|
413
|
+
| Use Case | Pattern |
|
|
414
|
+
|---|---|
|
|
415
|
+
| Only OGC CRS84 | `^http://www\\.opengis\\.net/def/crs/OGC/1\\.3/CRS84$` |
|
|
416
|
+
| Any OGC or EPSG CRS | `^http://www\\.opengis\\.net/def/crs/(OGC\|EPSG)/.*$` |
|
|
417
|
+
| Any valid CRS URI | `^http://www\\.opengis\\.net/def/crs/.*$` |
|
|
418
|
+
| ISO-8601 TRS family | `^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$` |
|
|
419
|
+
| Lowercase snake_case names | `^[a-z][a-z0-9_]*$` |
|
|
420
|
+
| CF standard name style | `^[a-z][a-z0-9_]*(_[a-z0-9]+)*$` |
|
|
421
|
+
| WMO-style alphanumeric | `^[A-Za-z][A-Za-z0-9_]*$` |
|
|
422
|
+
| Lowercase with hyphens | `^[a-z][a-z0-9\\-]*$` |
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
296
426
|
## Config Reference
|
|
297
427
|
|
|
298
428
|
### Top-level fields
|
|
@@ -313,7 +443,8 @@ The skipped tests are optional features not implemented by the server.
|
|
|
313
443
|
| `required_conformance_classes` | `list[string]` | no | Conformance classes that implementations must declare. Defaults to EDR Core |
|
|
314
444
|
| `extent_requirements` | `object` | no | Profile-level extent restrictions (see below) |
|
|
315
445
|
| `output_formats` | `list` | no | Profile-level output format definitions with schema references (see below) |
|
|
316
|
-
| `collection_id_pattern` | `string` | no | Regex pattern
|
|
446
|
+
| `collection_id_pattern` | `string` | no | Regex pattern that all collection IDs must match (validated at build time) |
|
|
447
|
+
| `parameter_name_pattern` | `string` | no | Regex pattern that all `parameter_names` keys must match (validated at build time) |
|
|
317
448
|
|
|
318
449
|
---
|
|
319
450
|
|
|
@@ -376,7 +507,7 @@ parameter_names:
|
|
|
376
507
|
|
|
377
508
|
### `extent_requirements`
|
|
378
509
|
|
|
379
|
-
Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent.
|
|
510
|
+
Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent. These constraints are **enforced at profile build time** — if any collection's CRS, TRS, or VRS value violates the rules here, the profile will be rejected with a clear error message. The constraints are also **embedded in the generated OpenAPI** so that downstream tools (schemathesis, CITE tests, client SDKs) can enforce them at runtime.
|
|
380
511
|
|
|
381
512
|
| Field | Type | Required | Description |
|
|
382
513
|
|---|---|---|---|
|
|
@@ -390,6 +521,8 @@ Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent.
|
|
|
390
521
|
|
|
391
522
|
**Note:** Either `allowed_crs` or `crs_pattern` must be specified.
|
|
392
523
|
|
|
524
|
+
**Enum approach** — lock down to specific values:
|
|
525
|
+
|
|
393
526
|
```yaml
|
|
394
527
|
extent_requirements:
|
|
395
528
|
minimum_bbox: [-180, -90, 180, 90]
|
|
@@ -400,6 +533,19 @@ extent_requirements:
|
|
|
400
533
|
- "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian"
|
|
401
534
|
```
|
|
402
535
|
|
|
536
|
+
**Regex approach** — allow any CRS from a family:
|
|
537
|
+
|
|
538
|
+
```yaml
|
|
539
|
+
extent_requirements:
|
|
540
|
+
minimum_bbox: [-180, -90, 180, 90]
|
|
541
|
+
# Accept any OGC or EPSG CRS
|
|
542
|
+
crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
|
|
543
|
+
# Accept any ISO-8601 TRS
|
|
544
|
+
trs_pattern: "^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$"
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Both approaches can coexist — if both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both** constraints.
|
|
548
|
+
|
|
403
549
|
---
|
|
404
550
|
|
|
405
551
|
### `output_formats[]`
|
|
@@ -561,12 +707,23 @@ This tool implements the requirements of OGC API - EDR Part 3: Service Profiles
|
|
|
561
707
|
4. **Extent Requirements** (REQ_extent)
|
|
562
708
|
- Profile-level `extent_requirements` specify minimum bounds
|
|
563
709
|
- CRS/TRS/VRS restrictions via enumerated lists or regex patterns
|
|
710
|
+
- **Enforced at build time**: collection CRS/TRS/VRS values are validated against `allowed_*` lists and `*_pattern` regexes
|
|
711
|
+
- **Propagated to OpenAPI**: constraints appear as `enum` or `pattern` in the generated collection response schemas
|
|
712
|
+
|
|
713
|
+
5. **Parameter Names** (REQ_parameter-names)
|
|
714
|
+
- Validates that all parameters specify `unit` and `observedProperty`
|
|
715
|
+
- Optional `parameter_name_pattern` enforces naming conventions across all collections
|
|
716
|
+
- Pattern constraints are embedded in the generated OpenAPI as `propertyNames.pattern`
|
|
717
|
+
|
|
718
|
+
6. **Collection ID Pattern**
|
|
719
|
+
- Optional `collection_id_pattern` enforces naming conventions for collection IDs
|
|
720
|
+
- Validated at build time via `re.fullmatch()`
|
|
564
721
|
|
|
565
|
-
|
|
722
|
+
7. **Output Formats** (REQ_output-format)
|
|
566
723
|
- Profile-level `output_formats` with schema references
|
|
567
724
|
- Links to JSON Schema, XML Schema, or format specifications
|
|
568
725
|
|
|
569
|
-
|
|
726
|
+
8. **Pub/Sub** (REQ_pubsub)
|
|
570
727
|
- Automatically adds Part 2 conformance requirement when `pubsub` is present
|
|
571
728
|
- AsyncAPI document specifies channels and payloads
|
|
572
729
|
|
|
@@ -619,7 +776,7 @@ generate(profile, Path("./output"))
|
|
|
619
776
|
|
|
620
777
|
## License
|
|
621
778
|
|
|
622
|
-
|
|
779
|
+
Apache — See [LICENSE](LICENSE) for details.
|
|
623
780
|
|
|
624
781
|
## Contact
|
|
625
782
|
|
|
@@ -18,6 +18,9 @@ pip install oapi-profile-builder
|
|
|
18
18
|
|
|
19
19
|
## Workflow
|
|
20
20
|
|
|
21
|
+
<img width="1001" height="721" alt="OGC API Service Profile Builder - Pydantic Validation Architecture drawio" src="https://github.com/user-attachments/assets/092c3dfc-549e-41b0-8a92-af0b89689950" />
|
|
22
|
+
|
|
23
|
+
|
|
21
24
|
### 1. Author a Profile Config
|
|
22
25
|
|
|
23
26
|
A profile config is a YAML or JSON file. Start with the minimal example:
|
|
@@ -248,6 +251,133 @@ The skipped tests are optional features not implemented by the server.
|
|
|
248
251
|
|
|
249
252
|
---
|
|
250
253
|
|
|
254
|
+
## Profile Configuration Guide
|
|
255
|
+
|
|
256
|
+
This section explains what is and isn't allowed when creating a profile, and how the tool validates your configuration.
|
|
257
|
+
|
|
258
|
+
### What Gets Validated
|
|
259
|
+
|
|
260
|
+
When you run `generate` or `validate`, the tool instantiates a `ServiceProfile` Pydantic model that enforces all of the following rules before any files are written. If any rule is violated, you get a clear error message pointing to the offending field.
|
|
261
|
+
|
|
262
|
+
#### Profile-Level Fields
|
|
263
|
+
|
|
264
|
+
| Field | Rules |
|
|
265
|
+
|---|---|
|
|
266
|
+
| `name` | Must match `^[a-z0-9_]+$` — lowercase letters, digits, and underscores only. Used in OGC URIs. |
|
|
267
|
+
| `title` | Any non-empty string. |
|
|
268
|
+
| `version` | Any string. Defaults to `"1.0"`. |
|
|
269
|
+
| `collections` | At least one collection is required. No duplicate `id` values. |
|
|
270
|
+
|
|
271
|
+
#### Collection IDs
|
|
272
|
+
|
|
273
|
+
By default, collection IDs can be any string accepted by edr-pydantic. To enforce a naming convention across all collections, use `collection_id_pattern`:
|
|
274
|
+
|
|
275
|
+
```yaml
|
|
276
|
+
# Only allow lowercase snake_case collection IDs
|
|
277
|
+
collection_id_pattern: "^[a-z][a-z0-9_]*$"
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The pattern is matched using Python's `re.fullmatch()`, so it must match the **entire** ID string.
|
|
281
|
+
|
|
282
|
+
#### CRS, TRS, and VRS Constraints
|
|
283
|
+
|
|
284
|
+
Each collection declares a CRS in `extent.spatial.crs`, and optionally a TRS in `extent.temporal.trs` and VRS in `extent.vertical.vrs`. The profile can constrain these values in two ways:
|
|
285
|
+
|
|
286
|
+
**Enumerated list** — only the exact values listed are accepted:
|
|
287
|
+
|
|
288
|
+
```yaml
|
|
289
|
+
extent_requirements:
|
|
290
|
+
minimum_bbox: [-180, -90, 180, 90]
|
|
291
|
+
allowed_crs:
|
|
292
|
+
- "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
|
293
|
+
- "http://www.opengis.net/def/crs/EPSG/0/4326"
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Regex pattern** — any value matching the pattern is accepted:
|
|
297
|
+
|
|
298
|
+
```yaml
|
|
299
|
+
extent_requirements:
|
|
300
|
+
minimum_bbox: [-180, -90, 180, 90]
|
|
301
|
+
# Accept any OGC or EPSG CRS
|
|
302
|
+
crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
If both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both**. At least one of `allowed_crs` or `crs_pattern` is required when `extent_requirements` is present.
|
|
306
|
+
|
|
307
|
+
The same enum/regex approach works for TRS (`allowed_trs` / `trs_pattern`) and VRS (`allowed_vrs` / `vrs_pattern`).
|
|
308
|
+
|
|
309
|
+
#### Parameter Name Constraints
|
|
310
|
+
|
|
311
|
+
By default, parameter names (the keys in `parameter_names`) can be any string. To enforce a naming convention, use `parameter_name_pattern`:
|
|
312
|
+
|
|
313
|
+
```yaml
|
|
314
|
+
# CF-style lowercase parameter names
|
|
315
|
+
parameter_name_pattern: "^[a-z][a-z0-9_]*$"
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```yaml
|
|
319
|
+
# Allow uppercase abbreviations like WMO codes
|
|
320
|
+
parameter_name_pattern: "^[A-Za-z][A-Za-z0-9_]*$"
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Every key in every collection's `parameter_names` must match this pattern. The pattern uses `re.fullmatch()`.
|
|
324
|
+
|
|
325
|
+
Additionally, per OGC API - EDR Part 3, every parameter must specify both `unit` and `observedProperty`. The tool enforces this automatically.
|
|
326
|
+
|
|
327
|
+
#### Requirement and Test IDs
|
|
328
|
+
|
|
329
|
+
| Field | Rules |
|
|
330
|
+
|---|---|
|
|
331
|
+
| Requirement `id` | Must match `^[a-z0-9][a-z0-9\-]*$` — lowercase, digits, hyphens. Cannot end with a hyphen. |
|
|
332
|
+
| AbstractTest `id` | Must exactly equal its `requirement_id`. |
|
|
333
|
+
| AbstractTest `requirement_id` | Must reference an existing requirement `id`. |
|
|
334
|
+
|
|
335
|
+
#### What Happens When Validation Fails
|
|
336
|
+
|
|
337
|
+
The tool prints a Pydantic validation error with the field path and a human-readable message. For example:
|
|
338
|
+
|
|
339
|
+
```
|
|
340
|
+
Value error, Collection 'my_data' CRS 'urn:ogc:def:crs:EPSG::4326'
|
|
341
|
+
does not match crs_pattern '^http://www\.opengis\.net/def/crs/(OGC|EPSG)/.*$'
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
```
|
|
345
|
+
Value error, Parameter name 'WIND_SPEED' in collection 'weather'
|
|
346
|
+
does not match parameter_name_pattern '^[a-z][a-z0-9_]*$'
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
Value error, Collection id 'My-Collection' does not match
|
|
351
|
+
collection_id_pattern '^[a-z][a-z0-9_]*$'
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### How Patterns Flow Into the Generated OpenAPI
|
|
355
|
+
|
|
356
|
+
When you specify `crs_pattern`, `allowed_crs`, or `parameter_name_pattern`, those constraints are embedded in the generated `openapi.yaml` so that runtime validation tools can enforce them:
|
|
357
|
+
|
|
358
|
+
- `crs_pattern` → `pattern` on the CRS string schema in collection responses
|
|
359
|
+
- `allowed_crs` → `enum` on the CRS string schema
|
|
360
|
+
- `trs_pattern` / `allowed_trs` → `pattern` / `enum` on the TRS field in extent.temporal
|
|
361
|
+
- `vrs_pattern` / `allowed_vrs` → `pattern` / `enum` on the VRS field in extent.vertical
|
|
362
|
+
- `parameter_name_pattern` → `propertyNames.pattern` on the `parameter_names` object schema
|
|
363
|
+
|
|
364
|
+
This means schemathesis, CITE tests, and client SDKs can validate server responses against these constraints without needing access to the original profile YAML.
|
|
365
|
+
|
|
366
|
+
### Quick Reference: Regex Examples
|
|
367
|
+
|
|
368
|
+
| Use Case | Pattern |
|
|
369
|
+
|---|---|
|
|
370
|
+
| Only OGC CRS84 | `^http://www\\.opengis\\.net/def/crs/OGC/1\\.3/CRS84$` |
|
|
371
|
+
| Any OGC or EPSG CRS | `^http://www\\.opengis\\.net/def/crs/(OGC\|EPSG)/.*$` |
|
|
372
|
+
| Any valid CRS URI | `^http://www\\.opengis\\.net/def/crs/.*$` |
|
|
373
|
+
| ISO-8601 TRS family | `^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$` |
|
|
374
|
+
| Lowercase snake_case names | `^[a-z][a-z0-9_]*$` |
|
|
375
|
+
| CF standard name style | `^[a-z][a-z0-9_]*(_[a-z0-9]+)*$` |
|
|
376
|
+
| WMO-style alphanumeric | `^[A-Za-z][A-Za-z0-9_]*$` |
|
|
377
|
+
| Lowercase with hyphens | `^[a-z][a-z0-9\\-]*$` |
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
251
381
|
## Config Reference
|
|
252
382
|
|
|
253
383
|
### Top-level fields
|
|
@@ -268,7 +398,8 @@ The skipped tests are optional features not implemented by the server.
|
|
|
268
398
|
| `required_conformance_classes` | `list[string]` | no | Conformance classes that implementations must declare. Defaults to EDR Core |
|
|
269
399
|
| `extent_requirements` | `object` | no | Profile-level extent restrictions (see below) |
|
|
270
400
|
| `output_formats` | `list` | no | Profile-level output format definitions with schema references (see below) |
|
|
271
|
-
| `collection_id_pattern` | `string` | no | Regex pattern
|
|
401
|
+
| `collection_id_pattern` | `string` | no | Regex pattern that all collection IDs must match (validated at build time) |
|
|
402
|
+
| `parameter_name_pattern` | `string` | no | Regex pattern that all `parameter_names` keys must match (validated at build time) |
|
|
272
403
|
|
|
273
404
|
---
|
|
274
405
|
|
|
@@ -331,7 +462,7 @@ parameter_names:
|
|
|
331
462
|
|
|
332
463
|
### `extent_requirements`
|
|
333
464
|
|
|
334
|
-
Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent.
|
|
465
|
+
Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent. These constraints are **enforced at profile build time** — if any collection's CRS, TRS, or VRS value violates the rules here, the profile will be rejected with a clear error message. The constraints are also **embedded in the generated OpenAPI** so that downstream tools (schemathesis, CITE tests, client SDKs) can enforce them at runtime.
|
|
335
466
|
|
|
336
467
|
| Field | Type | Required | Description |
|
|
337
468
|
|---|---|---|---|
|
|
@@ -345,6 +476,8 @@ Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent.
|
|
|
345
476
|
|
|
346
477
|
**Note:** Either `allowed_crs` or `crs_pattern` must be specified.
|
|
347
478
|
|
|
479
|
+
**Enum approach** — lock down to specific values:
|
|
480
|
+
|
|
348
481
|
```yaml
|
|
349
482
|
extent_requirements:
|
|
350
483
|
minimum_bbox: [-180, -90, 180, 90]
|
|
@@ -355,6 +488,19 @@ extent_requirements:
|
|
|
355
488
|
- "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian"
|
|
356
489
|
```
|
|
357
490
|
|
|
491
|
+
**Regex approach** — allow any CRS from a family:
|
|
492
|
+
|
|
493
|
+
```yaml
|
|
494
|
+
extent_requirements:
|
|
495
|
+
minimum_bbox: [-180, -90, 180, 90]
|
|
496
|
+
# Accept any OGC or EPSG CRS
|
|
497
|
+
crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
|
|
498
|
+
# Accept any ISO-8601 TRS
|
|
499
|
+
trs_pattern: "^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$"
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Both approaches can coexist — if both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both** constraints.
|
|
503
|
+
|
|
358
504
|
---
|
|
359
505
|
|
|
360
506
|
### `output_formats[]`
|
|
@@ -516,12 +662,23 @@ This tool implements the requirements of OGC API - EDR Part 3: Service Profiles
|
|
|
516
662
|
4. **Extent Requirements** (REQ_extent)
|
|
517
663
|
- Profile-level `extent_requirements` specify minimum bounds
|
|
518
664
|
- CRS/TRS/VRS restrictions via enumerated lists or regex patterns
|
|
665
|
+
- **Enforced at build time**: collection CRS/TRS/VRS values are validated against `allowed_*` lists and `*_pattern` regexes
|
|
666
|
+
- **Propagated to OpenAPI**: constraints appear as `enum` or `pattern` in the generated collection response schemas
|
|
667
|
+
|
|
668
|
+
5. **Parameter Names** (REQ_parameter-names)
|
|
669
|
+
- Validates that all parameters specify `unit` and `observedProperty`
|
|
670
|
+
- Optional `parameter_name_pattern` enforces naming conventions across all collections
|
|
671
|
+
- Pattern constraints are embedded in the generated OpenAPI as `propertyNames.pattern`
|
|
672
|
+
|
|
673
|
+
6. **Collection ID Pattern**
|
|
674
|
+
- Optional `collection_id_pattern` enforces naming conventions for collection IDs
|
|
675
|
+
- Validated at build time via `re.fullmatch()`
|
|
519
676
|
|
|
520
|
-
|
|
677
|
+
7. **Output Formats** (REQ_output-format)
|
|
521
678
|
- Profile-level `output_formats` with schema references
|
|
522
679
|
- Links to JSON Schema, XML Schema, or format specifications
|
|
523
680
|
|
|
524
|
-
|
|
681
|
+
8. **Pub/Sub** (REQ_pubsub)
|
|
525
682
|
- Automatically adds Part 2 conformance requirement when `pubsub` is present
|
|
526
683
|
- AsyncAPI document specifies channels and payloads
|
|
527
684
|
|
|
@@ -574,7 +731,7 @@ generate(profile, Path("./output"))
|
|
|
574
731
|
|
|
575
732
|
## License
|
|
576
733
|
|
|
577
|
-
|
|
734
|
+
Apache — See [LICENSE](LICENSE) for details.
|
|
578
735
|
|
|
579
736
|
## Contact
|
|
580
737
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "oapi-profile-builder"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.3"
|
|
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.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/generate.py
RENAMED
|
@@ -176,12 +176,111 @@ _QUERY_PARAMS: dict[str, list[dict]] = {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
|
|
179
|
-
def
|
|
179
|
+
def _collection_response_schema(coll: Collection,
|
|
180
|
+
profile: "ServiceProfile | None") -> dict:
|
|
181
|
+
"""Build a 200 response schema for a single collection endpoint.
|
|
182
|
+
|
|
183
|
+
When the profile specifies extent_requirements (allowed_crs / crs_pattern,
|
|
184
|
+
allowed_trs / trs_pattern, allowed_vrs / vrs_pattern) or a
|
|
185
|
+
parameter_name_pattern, those constraints are embedded in the response
|
|
186
|
+
schema so that downstream tools (schemathesis, CITE, client SDKs) can
|
|
187
|
+
enforce them at runtime.
|
|
188
|
+
"""
|
|
189
|
+
# Start with the base collection schema
|
|
190
|
+
crs_schema: dict = {"type": "string"}
|
|
191
|
+
param_name_schema: dict = {"type": "string"}
|
|
192
|
+
|
|
193
|
+
if profile and profile.extent_requirements:
|
|
194
|
+
er = profile.extent_requirements
|
|
195
|
+
if er.allowed_crs:
|
|
196
|
+
crs_schema["enum"] = er.allowed_crs
|
|
197
|
+
elif er.crs_pattern:
|
|
198
|
+
crs_schema["pattern"] = er.crs_pattern
|
|
199
|
+
|
|
200
|
+
if profile and profile.parameter_name_pattern:
|
|
201
|
+
param_name_schema["pattern"] = profile.parameter_name_pattern
|
|
202
|
+
|
|
203
|
+
schema: dict = {
|
|
204
|
+
"type": "object",
|
|
205
|
+
"required": ["id"],
|
|
206
|
+
"properties": {
|
|
207
|
+
"id": {"type": "string"},
|
|
208
|
+
"title": {"type": "string"},
|
|
209
|
+
"description": {"type": "string"},
|
|
210
|
+
"links": _LINKS_ARRAY,
|
|
211
|
+
"crs": {
|
|
212
|
+
"type": "array",
|
|
213
|
+
"items": crs_schema,
|
|
214
|
+
},
|
|
215
|
+
"parameter_names": {
|
|
216
|
+
"type": "object",
|
|
217
|
+
"additionalProperties": {"type": "object"},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
# If we have a parameter_name_pattern, constrain the property names
|
|
223
|
+
if profile and profile.parameter_name_pattern:
|
|
224
|
+
schema["properties"]["parameter_names"]["propertyNames"] = param_name_schema
|
|
225
|
+
|
|
226
|
+
# Embed TRS constraint in extent.temporal if present
|
|
227
|
+
if profile and profile.extent_requirements:
|
|
228
|
+
er = profile.extent_requirements
|
|
229
|
+
trs_schema: dict = {"type": "string"}
|
|
230
|
+
if er.allowed_trs:
|
|
231
|
+
trs_schema["enum"] = er.allowed_trs
|
|
232
|
+
elif er.trs_pattern:
|
|
233
|
+
trs_schema["pattern"] = er.trs_pattern
|
|
234
|
+
|
|
235
|
+
vrs_schema: dict = {"type": "string"}
|
|
236
|
+
if er.allowed_vrs:
|
|
237
|
+
vrs_schema["enum"] = er.allowed_vrs
|
|
238
|
+
elif er.vrs_pattern:
|
|
239
|
+
vrs_schema["pattern"] = er.vrs_pattern
|
|
240
|
+
|
|
241
|
+
schema["properties"]["extent"] = {
|
|
242
|
+
"type": "object",
|
|
243
|
+
"properties": {
|
|
244
|
+
"spatial": {
|
|
245
|
+
"type": "object",
|
|
246
|
+
"properties": {
|
|
247
|
+
"bbox": {"type": "array"},
|
|
248
|
+
"crs": crs_schema,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
"temporal": {
|
|
252
|
+
"type": "object",
|
|
253
|
+
"properties": {
|
|
254
|
+
"trs": trs_schema,
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
"vertical": {
|
|
258
|
+
"type": "object",
|
|
259
|
+
"properties": {
|
|
260
|
+
"vrs": vrs_schema,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
"description": "Collection metadata",
|
|
268
|
+
"content": {"application/json": {"schema": schema}},
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _collection_paths(coll: Collection, examples: dict | None = None,
|
|
273
|
+
profile: ServiceProfile | None = None) -> dict:
|
|
180
274
|
paths: dict = {}
|
|
181
275
|
base = f"/collections/{coll.id}"
|
|
182
276
|
tag = coll.id
|
|
183
277
|
desc = getattr(coll, "description", None) or coll.id
|
|
184
278
|
|
|
279
|
+
# Build a collection-specific response schema that includes CRS and
|
|
280
|
+
# parameter-name constraints from the profile's extent_requirements
|
|
281
|
+
# and parameter_name_pattern.
|
|
282
|
+
coll_schema = _collection_response_schema(coll, profile)
|
|
283
|
+
|
|
185
284
|
paths[base] = {"get": {
|
|
186
285
|
"summary": f"Get {coll.title or coll.id} metadata",
|
|
187
286
|
"description": desc,
|
|
@@ -189,7 +288,7 @@ def _collection_paths(coll: Collection, examples: dict | None = None) -> dict:
|
|
|
189
288
|
"tags": [tag],
|
|
190
289
|
"parameters": [_F, _LANG],
|
|
191
290
|
"responses": {
|
|
192
|
-
"200":
|
|
291
|
+
"200": coll_schema,
|
|
193
292
|
"400": _ERR_400, "404": _ERR_404, "500": _ERR_500,
|
|
194
293
|
},
|
|
195
294
|
}}
|
|
@@ -477,7 +576,7 @@ def _processes_paths(profile: ServiceProfile) -> dict:
|
|
|
477
576
|
def build_openapi(profile: ServiceProfile) -> dict:
|
|
478
577
|
paths: dict = _core_paths(profile)
|
|
479
578
|
for coll in profile.collections:
|
|
480
|
-
paths.update(_collection_paths(coll, profile.collection_examples.get(coll.id)))
|
|
579
|
+
paths.update(_collection_paths(coll, profile.collection_examples.get(coll.id), profile))
|
|
481
580
|
paths.update(_processes_paths(profile))
|
|
482
581
|
|
|
483
582
|
tags = [{"name": "server", "description": profile.title}]
|
{oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/models.py
RENAMED
|
@@ -38,6 +38,7 @@ so that EDR data model types are authoritative and shared with the broader ecosy
|
|
|
38
38
|
|
|
39
39
|
from __future__ import annotations
|
|
40
40
|
|
|
41
|
+
import re
|
|
41
42
|
from enum import Enum
|
|
42
43
|
from typing import Annotated, Literal
|
|
43
44
|
|
|
@@ -201,6 +202,10 @@ class ServiceProfile(BaseModel):
|
|
|
201
202
|
default=None,
|
|
202
203
|
description="Regex pattern for valid collection IDs"
|
|
203
204
|
)
|
|
205
|
+
parameter_name_pattern: str | None = Field(
|
|
206
|
+
default=None,
|
|
207
|
+
description="Regex pattern that all parameter_names keys must match"
|
|
208
|
+
)
|
|
204
209
|
|
|
205
210
|
# OGC identifiers derived from name — not user-supplied
|
|
206
211
|
@property
|
|
@@ -270,3 +275,113 @@ class ServiceProfile(BaseModel):
|
|
|
270
275
|
)
|
|
271
276
|
)
|
|
272
277
|
return self
|
|
278
|
+
|
|
279
|
+
@model_validator(mode="after")
|
|
280
|
+
def validate_collection_id_pattern(self) -> ServiceProfile:
|
|
281
|
+
"""Validate collection IDs against collection_id_pattern if specified."""
|
|
282
|
+
if not self.collection_id_pattern:
|
|
283
|
+
return self
|
|
284
|
+
try:
|
|
285
|
+
pat = re.compile(self.collection_id_pattern)
|
|
286
|
+
except re.error as exc:
|
|
287
|
+
raise ValueError(f"Invalid collection_id_pattern regex: {exc}") from exc
|
|
288
|
+
for coll in self.collections:
|
|
289
|
+
if not pat.fullmatch(coll.id):
|
|
290
|
+
raise ValueError(
|
|
291
|
+
f"Collection id '{coll.id}' does not match "
|
|
292
|
+
f"collection_id_pattern '{self.collection_id_pattern}'"
|
|
293
|
+
)
|
|
294
|
+
return self
|
|
295
|
+
|
|
296
|
+
@model_validator(mode="after")
|
|
297
|
+
def validate_collection_extent_patterns(self) -> ServiceProfile:
|
|
298
|
+
"""Validate collection CRS/TRS/VRS values against extent_requirements patterns and enums."""
|
|
299
|
+
if not self.extent_requirements:
|
|
300
|
+
return self
|
|
301
|
+
er = self.extent_requirements
|
|
302
|
+
|
|
303
|
+
# Compile patterns once, validating regex syntax
|
|
304
|
+
crs_pat = _compile_optional_pattern("crs_pattern", er.crs_pattern)
|
|
305
|
+
trs_pat = _compile_optional_pattern("trs_pattern", er.trs_pattern)
|
|
306
|
+
vrs_pat = _compile_optional_pattern("vrs_pattern", er.vrs_pattern)
|
|
307
|
+
|
|
308
|
+
for coll in self.collections:
|
|
309
|
+
# --- CRS ---
|
|
310
|
+
crs = coll.extent.spatial.crs if coll.extent and coll.extent.spatial else None
|
|
311
|
+
if crs:
|
|
312
|
+
if er.allowed_crs and crs not in er.allowed_crs:
|
|
313
|
+
raise ValueError(
|
|
314
|
+
f"Collection '{coll.id}' CRS '{crs}' is not in allowed_crs "
|
|
315
|
+
f"{er.allowed_crs}"
|
|
316
|
+
)
|
|
317
|
+
if crs_pat and not crs_pat.fullmatch(crs):
|
|
318
|
+
raise ValueError(
|
|
319
|
+
f"Collection '{coll.id}' CRS '{crs}' does not match "
|
|
320
|
+
f"crs_pattern '{er.crs_pattern}'"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# --- TRS ---
|
|
324
|
+
trs = (
|
|
325
|
+
coll.extent.temporal.trs
|
|
326
|
+
if coll.extent and coll.extent.temporal else None
|
|
327
|
+
)
|
|
328
|
+
if trs:
|
|
329
|
+
if er.allowed_trs and trs not in er.allowed_trs:
|
|
330
|
+
raise ValueError(
|
|
331
|
+
f"Collection '{coll.id}' TRS '{trs}' is not in allowed_trs "
|
|
332
|
+
f"{er.allowed_trs}"
|
|
333
|
+
)
|
|
334
|
+
if trs_pat and not trs_pat.fullmatch(trs):
|
|
335
|
+
raise ValueError(
|
|
336
|
+
f"Collection '{coll.id}' TRS '{trs}' does not match "
|
|
337
|
+
f"trs_pattern '{er.trs_pattern}'"
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# --- VRS ---
|
|
341
|
+
vrs = (
|
|
342
|
+
coll.extent.vertical.vrs
|
|
343
|
+
if coll.extent and coll.extent.vertical else None
|
|
344
|
+
)
|
|
345
|
+
if vrs:
|
|
346
|
+
if er.allowed_vrs and vrs not in er.allowed_vrs:
|
|
347
|
+
raise ValueError(
|
|
348
|
+
f"Collection '{coll.id}' VRS '{vrs}' is not in allowed_vrs "
|
|
349
|
+
f"{er.allowed_vrs}"
|
|
350
|
+
)
|
|
351
|
+
if vrs_pat and not vrs_pat.fullmatch(vrs):
|
|
352
|
+
raise ValueError(
|
|
353
|
+
f"Collection '{coll.id}' VRS '{vrs}' does not match "
|
|
354
|
+
f"vrs_pattern '{er.vrs_pattern}'"
|
|
355
|
+
)
|
|
356
|
+
return self
|
|
357
|
+
|
|
358
|
+
@model_validator(mode="after")
|
|
359
|
+
def validate_parameter_name_patterns(self) -> ServiceProfile:
|
|
360
|
+
"""Validate parameter_names keys against parameter_name_pattern if specified."""
|
|
361
|
+
if not self.parameter_name_pattern:
|
|
362
|
+
return self
|
|
363
|
+
try:
|
|
364
|
+
pat = re.compile(self.parameter_name_pattern)
|
|
365
|
+
except re.error as exc:
|
|
366
|
+
raise ValueError(f"Invalid parameter_name_pattern regex: {exc}") from exc
|
|
367
|
+
for coll in self.collections:
|
|
368
|
+
if not coll.parameter_names:
|
|
369
|
+
continue
|
|
370
|
+
for name in coll.parameter_names.root:
|
|
371
|
+
if not pat.fullmatch(name):
|
|
372
|
+
raise ValueError(
|
|
373
|
+
f"Parameter name '{name}' in collection '{coll.id}' "
|
|
374
|
+
f"does not match parameter_name_pattern "
|
|
375
|
+
f"'{self.parameter_name_pattern}'"
|
|
376
|
+
)
|
|
377
|
+
return self
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _compile_optional_pattern(label: str, pattern: str | None) -> re.Pattern | None:
|
|
381
|
+
"""Compile a regex pattern string, raising ValueError on invalid syntax."""
|
|
382
|
+
if pattern is None:
|
|
383
|
+
return None
|
|
384
|
+
try:
|
|
385
|
+
return re.compile(pattern)
|
|
386
|
+
except re.error as exc:
|
|
387
|
+
raise ValueError(f"Invalid {label} regex '{pattern}': {exc}") from exc
|
{oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3/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: 2.0.
|
|
3
|
+
Version: 2.0.3
|
|
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
|
|
@@ -63,6 +63,9 @@ pip install oapi-profile-builder
|
|
|
63
63
|
|
|
64
64
|
## Workflow
|
|
65
65
|
|
|
66
|
+
<img width="1001" height="721" alt="OGC API Service Profile Builder - Pydantic Validation Architecture drawio" src="https://github.com/user-attachments/assets/092c3dfc-549e-41b0-8a92-af0b89689950" />
|
|
67
|
+
|
|
68
|
+
|
|
66
69
|
### 1. Author a Profile Config
|
|
67
70
|
|
|
68
71
|
A profile config is a YAML or JSON file. Start with the minimal example:
|
|
@@ -293,6 +296,133 @@ The skipped tests are optional features not implemented by the server.
|
|
|
293
296
|
|
|
294
297
|
---
|
|
295
298
|
|
|
299
|
+
## Profile Configuration Guide
|
|
300
|
+
|
|
301
|
+
This section explains what is and isn't allowed when creating a profile, and how the tool validates your configuration.
|
|
302
|
+
|
|
303
|
+
### What Gets Validated
|
|
304
|
+
|
|
305
|
+
When you run `generate` or `validate`, the tool instantiates a `ServiceProfile` Pydantic model that enforces all of the following rules before any files are written. If any rule is violated, you get a clear error message pointing to the offending field.
|
|
306
|
+
|
|
307
|
+
#### Profile-Level Fields
|
|
308
|
+
|
|
309
|
+
| Field | Rules |
|
|
310
|
+
|---|---|
|
|
311
|
+
| `name` | Must match `^[a-z0-9_]+$` — lowercase letters, digits, and underscores only. Used in OGC URIs. |
|
|
312
|
+
| `title` | Any non-empty string. |
|
|
313
|
+
| `version` | Any string. Defaults to `"1.0"`. |
|
|
314
|
+
| `collections` | At least one collection is required. No duplicate `id` values. |
|
|
315
|
+
|
|
316
|
+
#### Collection IDs
|
|
317
|
+
|
|
318
|
+
By default, collection IDs can be any string accepted by edr-pydantic. To enforce a naming convention across all collections, use `collection_id_pattern`:
|
|
319
|
+
|
|
320
|
+
```yaml
|
|
321
|
+
# Only allow lowercase snake_case collection IDs
|
|
322
|
+
collection_id_pattern: "^[a-z][a-z0-9_]*$"
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
The pattern is matched using Python's `re.fullmatch()`, so it must match the **entire** ID string.
|
|
326
|
+
|
|
327
|
+
#### CRS, TRS, and VRS Constraints
|
|
328
|
+
|
|
329
|
+
Each collection declares a CRS in `extent.spatial.crs`, and optionally a TRS in `extent.temporal.trs` and VRS in `extent.vertical.vrs`. The profile can constrain these values in two ways:
|
|
330
|
+
|
|
331
|
+
**Enumerated list** — only the exact values listed are accepted:
|
|
332
|
+
|
|
333
|
+
```yaml
|
|
334
|
+
extent_requirements:
|
|
335
|
+
minimum_bbox: [-180, -90, 180, 90]
|
|
336
|
+
allowed_crs:
|
|
337
|
+
- "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
|
|
338
|
+
- "http://www.opengis.net/def/crs/EPSG/0/4326"
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Regex pattern** — any value matching the pattern is accepted:
|
|
342
|
+
|
|
343
|
+
```yaml
|
|
344
|
+
extent_requirements:
|
|
345
|
+
minimum_bbox: [-180, -90, 180, 90]
|
|
346
|
+
# Accept any OGC or EPSG CRS
|
|
347
|
+
crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
If both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both**. At least one of `allowed_crs` or `crs_pattern` is required when `extent_requirements` is present.
|
|
351
|
+
|
|
352
|
+
The same enum/regex approach works for TRS (`allowed_trs` / `trs_pattern`) and VRS (`allowed_vrs` / `vrs_pattern`).
|
|
353
|
+
|
|
354
|
+
#### Parameter Name Constraints
|
|
355
|
+
|
|
356
|
+
By default, parameter names (the keys in `parameter_names`) can be any string. To enforce a naming convention, use `parameter_name_pattern`:
|
|
357
|
+
|
|
358
|
+
```yaml
|
|
359
|
+
# CF-style lowercase parameter names
|
|
360
|
+
parameter_name_pattern: "^[a-z][a-z0-9_]*$"
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```yaml
|
|
364
|
+
# Allow uppercase abbreviations like WMO codes
|
|
365
|
+
parameter_name_pattern: "^[A-Za-z][A-Za-z0-9_]*$"
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Every key in every collection's `parameter_names` must match this pattern. The pattern uses `re.fullmatch()`.
|
|
369
|
+
|
|
370
|
+
Additionally, per OGC API - EDR Part 3, every parameter must specify both `unit` and `observedProperty`. The tool enforces this automatically.
|
|
371
|
+
|
|
372
|
+
#### Requirement and Test IDs
|
|
373
|
+
|
|
374
|
+
| Field | Rules |
|
|
375
|
+
|---|---|
|
|
376
|
+
| Requirement `id` | Must match `^[a-z0-9][a-z0-9\-]*$` — lowercase, digits, hyphens. Cannot end with a hyphen. |
|
|
377
|
+
| AbstractTest `id` | Must exactly equal its `requirement_id`. |
|
|
378
|
+
| AbstractTest `requirement_id` | Must reference an existing requirement `id`. |
|
|
379
|
+
|
|
380
|
+
#### What Happens When Validation Fails
|
|
381
|
+
|
|
382
|
+
The tool prints a Pydantic validation error with the field path and a human-readable message. For example:
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
Value error, Collection 'my_data' CRS 'urn:ogc:def:crs:EPSG::4326'
|
|
386
|
+
does not match crs_pattern '^http://www\.opengis\.net/def/crs/(OGC|EPSG)/.*$'
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```
|
|
390
|
+
Value error, Parameter name 'WIND_SPEED' in collection 'weather'
|
|
391
|
+
does not match parameter_name_pattern '^[a-z][a-z0-9_]*$'
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
Value error, Collection id 'My-Collection' does not match
|
|
396
|
+
collection_id_pattern '^[a-z][a-z0-9_]*$'
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### How Patterns Flow Into the Generated OpenAPI
|
|
400
|
+
|
|
401
|
+
When you specify `crs_pattern`, `allowed_crs`, or `parameter_name_pattern`, those constraints are embedded in the generated `openapi.yaml` so that runtime validation tools can enforce them:
|
|
402
|
+
|
|
403
|
+
- `crs_pattern` → `pattern` on the CRS string schema in collection responses
|
|
404
|
+
- `allowed_crs` → `enum` on the CRS string schema
|
|
405
|
+
- `trs_pattern` / `allowed_trs` → `pattern` / `enum` on the TRS field in extent.temporal
|
|
406
|
+
- `vrs_pattern` / `allowed_vrs` → `pattern` / `enum` on the VRS field in extent.vertical
|
|
407
|
+
- `parameter_name_pattern` → `propertyNames.pattern` on the `parameter_names` object schema
|
|
408
|
+
|
|
409
|
+
This means schemathesis, CITE tests, and client SDKs can validate server responses against these constraints without needing access to the original profile YAML.
|
|
410
|
+
|
|
411
|
+
### Quick Reference: Regex Examples
|
|
412
|
+
|
|
413
|
+
| Use Case | Pattern |
|
|
414
|
+
|---|---|
|
|
415
|
+
| Only OGC CRS84 | `^http://www\\.opengis\\.net/def/crs/OGC/1\\.3/CRS84$` |
|
|
416
|
+
| Any OGC or EPSG CRS | `^http://www\\.opengis\\.net/def/crs/(OGC\|EPSG)/.*$` |
|
|
417
|
+
| Any valid CRS URI | `^http://www\\.opengis\\.net/def/crs/.*$` |
|
|
418
|
+
| ISO-8601 TRS family | `^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$` |
|
|
419
|
+
| Lowercase snake_case names | `^[a-z][a-z0-9_]*$` |
|
|
420
|
+
| CF standard name style | `^[a-z][a-z0-9_]*(_[a-z0-9]+)*$` |
|
|
421
|
+
| WMO-style alphanumeric | `^[A-Za-z][A-Za-z0-9_]*$` |
|
|
422
|
+
| Lowercase with hyphens | `^[a-z][a-z0-9\\-]*$` |
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
296
426
|
## Config Reference
|
|
297
427
|
|
|
298
428
|
### Top-level fields
|
|
@@ -313,7 +443,8 @@ The skipped tests are optional features not implemented by the server.
|
|
|
313
443
|
| `required_conformance_classes` | `list[string]` | no | Conformance classes that implementations must declare. Defaults to EDR Core |
|
|
314
444
|
| `extent_requirements` | `object` | no | Profile-level extent restrictions (see below) |
|
|
315
445
|
| `output_formats` | `list` | no | Profile-level output format definitions with schema references (see below) |
|
|
316
|
-
| `collection_id_pattern` | `string` | no | Regex pattern
|
|
446
|
+
| `collection_id_pattern` | `string` | no | Regex pattern that all collection IDs must match (validated at build time) |
|
|
447
|
+
| `parameter_name_pattern` | `string` | no | Regex pattern that all `parameter_names` keys must match (validated at build time) |
|
|
317
448
|
|
|
318
449
|
---
|
|
319
450
|
|
|
@@ -376,7 +507,7 @@ parameter_names:
|
|
|
376
507
|
|
|
377
508
|
### `extent_requirements`
|
|
378
509
|
|
|
379
|
-
Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent.
|
|
510
|
+
Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent. These constraints are **enforced at profile build time** — if any collection's CRS, TRS, or VRS value violates the rules here, the profile will be rejected with a clear error message. The constraints are also **embedded in the generated OpenAPI** so that downstream tools (schemathesis, CITE tests, client SDKs) can enforce them at runtime.
|
|
380
511
|
|
|
381
512
|
| Field | Type | Required | Description |
|
|
382
513
|
|---|---|---|---|
|
|
@@ -390,6 +521,8 @@ Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent.
|
|
|
390
521
|
|
|
391
522
|
**Note:** Either `allowed_crs` or `crs_pattern` must be specified.
|
|
392
523
|
|
|
524
|
+
**Enum approach** — lock down to specific values:
|
|
525
|
+
|
|
393
526
|
```yaml
|
|
394
527
|
extent_requirements:
|
|
395
528
|
minimum_bbox: [-180, -90, 180, 90]
|
|
@@ -400,6 +533,19 @@ extent_requirements:
|
|
|
400
533
|
- "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian"
|
|
401
534
|
```
|
|
402
535
|
|
|
536
|
+
**Regex approach** — allow any CRS from a family:
|
|
537
|
+
|
|
538
|
+
```yaml
|
|
539
|
+
extent_requirements:
|
|
540
|
+
minimum_bbox: [-180, -90, 180, 90]
|
|
541
|
+
# Accept any OGC or EPSG CRS
|
|
542
|
+
crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
|
|
543
|
+
# Accept any ISO-8601 TRS
|
|
544
|
+
trs_pattern: "^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$"
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Both approaches can coexist — if both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both** constraints.
|
|
548
|
+
|
|
403
549
|
---
|
|
404
550
|
|
|
405
551
|
### `output_formats[]`
|
|
@@ -561,12 +707,23 @@ This tool implements the requirements of OGC API - EDR Part 3: Service Profiles
|
|
|
561
707
|
4. **Extent Requirements** (REQ_extent)
|
|
562
708
|
- Profile-level `extent_requirements` specify minimum bounds
|
|
563
709
|
- CRS/TRS/VRS restrictions via enumerated lists or regex patterns
|
|
710
|
+
- **Enforced at build time**: collection CRS/TRS/VRS values are validated against `allowed_*` lists and `*_pattern` regexes
|
|
711
|
+
- **Propagated to OpenAPI**: constraints appear as `enum` or `pattern` in the generated collection response schemas
|
|
712
|
+
|
|
713
|
+
5. **Parameter Names** (REQ_parameter-names)
|
|
714
|
+
- Validates that all parameters specify `unit` and `observedProperty`
|
|
715
|
+
- Optional `parameter_name_pattern` enforces naming conventions across all collections
|
|
716
|
+
- Pattern constraints are embedded in the generated OpenAPI as `propertyNames.pattern`
|
|
717
|
+
|
|
718
|
+
6. **Collection ID Pattern**
|
|
719
|
+
- Optional `collection_id_pattern` enforces naming conventions for collection IDs
|
|
720
|
+
- Validated at build time via `re.fullmatch()`
|
|
564
721
|
|
|
565
|
-
|
|
722
|
+
7. **Output Formats** (REQ_output-format)
|
|
566
723
|
- Profile-level `output_formats` with schema references
|
|
567
724
|
- Links to JSON Schema, XML Schema, or format specifications
|
|
568
725
|
|
|
569
|
-
|
|
726
|
+
8. **Pub/Sub** (REQ_pubsub)
|
|
570
727
|
- Automatically adds Part 2 conformance requirement when `pubsub` is present
|
|
571
728
|
- AsyncAPI document specifies channels and payloads
|
|
572
729
|
|
|
@@ -619,7 +776,7 @@ generate(profile, Path("./output"))
|
|
|
619
776
|
|
|
620
777
|
## License
|
|
621
778
|
|
|
622
|
-
|
|
779
|
+
Apache — See [LICENSE](LICENSE) for details.
|
|
623
780
|
|
|
624
781
|
## Contact
|
|
625
782
|
|
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/src/oapi_profile_builder/cite_features.py
RENAMED
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-2.0.2 → oapi_profile_builder-2.0.3}/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
|