oapi-profile-builder 3.1.0__tar.gz → 3.2.1__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-3.1.0/src/oapi_profile_builder.egg-info → oapi_profile_builder-3.2.1}/PKG-INFO +138 -3
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/README.md +135 -2
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/pyproject.toml +2 -1
- oapi_profile_builder-3.2.1/src/oapi_profile_builder/cover.py +176 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder/generate.py +120 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder/models.py +165 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder/server_validation.py +91 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1/src/oapi_profile_builder.egg-info}/PKG-INFO +138 -3
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder.egg-info/SOURCES.txt +1 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder.egg-info/requires.txt +3 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/LICENSE +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/setup.cfg +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder/__init__.py +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder/cite.py +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder/cite_features.py +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder/cli.py +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder/compile.py +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder.egg-info/dependency_links.txt +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder.egg-info/entry_points.txt +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.1}/src/oapi_profile_builder.egg-info/top_level.txt +0 -0
{oapi_profile_builder-3.1.0/src/oapi_profile_builder.egg-info → oapi_profile_builder-3.2.1}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oapi-profile-builder
|
|
3
|
-
Version: 3.1
|
|
3
|
+
Version: 3.2.1
|
|
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
|
|
@@ -41,11 +41,13 @@ Requires-Dist: pyyaml>=6.0
|
|
|
41
41
|
Requires-Dist: requests>=2.31
|
|
42
42
|
Provides-Extra: validate
|
|
43
43
|
Requires-Dist: schemathesis>=3.0; extra == "validate"
|
|
44
|
+
Provides-Extra: pdf
|
|
45
|
+
Requires-Dist: pillow>=10.0; extra == "pdf"
|
|
44
46
|
Dynamic: license-file
|
|
45
47
|
|
|
46
48
|
# OGC API Service Profile Builder
|
|
47
49
|
|
|
48
|
-
Generate OGC API - EDR Part 3 Service Profile artifacts from a YAML config — OpenAPI 3.1.0, AsyncAPI, AsciiDoc requirements
|
|
50
|
+
Generate OGC API - EDR Part 3 Service Profile artifacts from a YAML config — OpenAPI 3.1.0, AsyncAPI, AsciiDoc requirements and conformance tests, and a publication-ready Metanorma PDF with optional custom (e.g. DGIWG) branding.
|
|
49
51
|
|
|
50
52
|
[](https://pypi.org/project/oapi-profile-builder/)
|
|
51
53
|
[](LICENSE)
|
|
@@ -80,13 +82,16 @@ output/
|
|
|
80
82
|
|
|
81
83
|
## Example Profiles
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
Six working examples are included:
|
|
84
86
|
|
|
85
87
|
| File | What it shows |
|
|
86
88
|
|---|---|
|
|
87
89
|
| [`examples/minimal_profile.yaml`](examples/minimal_profile.yaml) | Smallest valid profile — one collection, one requirement |
|
|
88
90
|
| [`examples/insitu_observations_profile.yaml`](examples/insitu_observations_profile.yaml) | Full meteorological profile — 8 parameters with QUDT units, CF standard names, metocean extensions, CRS listing, temporal extent, custom dimensions, `parameter_schema` |
|
|
91
|
+
| [`examples/nws_connect_profile.yaml`](examples/nws_connect_profile.yaml) | PubSub profile — generates `asyncapi.yaml` alongside the OpenAPI |
|
|
89
92
|
| [`examples/nwsviz_profile.yaml`](examples/nwsviz_profile.yaml) | Production profile — 13 collections, 3 OGC API Processes, PDF metadata |
|
|
93
|
+
| [`examples/nwp_radar.yaml`](examples/nwp_radar.yaml) | **DGIWG-branded** profile — custom cover logo, DGIWG colours/footer/legal page, `default_output_format`, per-query CRS, vertical `positive`, DGIWG namespace |
|
|
94
|
+
| [`examples/nwp_earth_observations_lightning_profile.yaml`](examples/nwp_earth_observations_lightning_profile.yaml) | DGIWG lightning profile — same branding, CF units |
|
|
90
95
|
|
|
91
96
|
---
|
|
92
97
|
|
|
@@ -115,6 +120,8 @@ Add `--pdf` to also compile an OGC-compliant PDF via the `metanorma/metanorma` D
|
|
|
115
120
|
oapi-profile-builder generate --config my_profile.yaml --output ./output --pdf
|
|
116
121
|
```
|
|
117
122
|
|
|
123
|
+
The PDF can be rebranded for another SDO (e.g. DGIWG) — custom cover logo, colours, legal page, footer, and identifier namespace — all from the profile config. See [Document generation and branding](#document-generation-and-branding).
|
|
124
|
+
|
|
118
125
|
### `validate`
|
|
119
126
|
|
|
120
127
|
Validates the config without writing any files. Useful in CI before generating.
|
|
@@ -219,6 +226,29 @@ jobs:
|
|
|
219
226
|
|
|
220
227
|
---
|
|
221
228
|
|
|
229
|
+
## Document generation and branding
|
|
230
|
+
|
|
231
|
+
With `--pdf`, the builder compiles a Metanorma document (`document.adoc` + `sections/`, `requirements/`, `abstract_tests/`) into `document.pdf` using the official `metanorma/metanorma` Docker image. By default this is an OGC `draft-standard` document.
|
|
232
|
+
|
|
233
|
+
Everything about the document — cover, title-page metadata, legal page, footer, colours, and even the requirement/conformance identifier namespace — is driven from the profile config, so the same tooling can produce a document branded for another SDO such as **DGIWG** with no OGC branding remaining.
|
|
234
|
+
|
|
235
|
+
| What you want to change | Field | Mechanism |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| Cover logo + tagline (replaces the OGC cover) | `document_metadata.cover` | Generated cover image via `:coverpage-image:` full replacement |
|
|
238
|
+
| Title, doc number, edition, dates, sub-title/status | `title`, `version`, `document_metadata.{doc_number, doc_type, doc_subtype, status, *_date}` | Metanorma document attributes |
|
|
239
|
+
| PDF colours (titles, tables, cover, backgrounds) | `document_metadata.colors` | `:presentation-metadata-color-*:` |
|
|
240
|
+
| Page-ii legal text (copyright / license / notice) | `document_metadata.boilerplate` | `:boilerplate-authority:` |
|
|
241
|
+
| Footer organisation name | `document_metadata.copyright_holder` | `:copyright-holder:` |
|
|
242
|
+
| Remove the OGC logo from the legal page | `document_metadata.suppress_flavor_logo` | targeted `:pdf-stylesheet-override:` |
|
|
243
|
+
| Requirement / conformance identifier namespace | `spec_uri_base` | derives `req_uri` / `conf_uri` |
|
|
244
|
+
| Submitter table, notice paragraph, normative refs | `document_metadata.{submitters, notice, normative_references}` | Metanorma attributes / sections |
|
|
245
|
+
|
|
246
|
+
Custom branding requires Pillow for the cover image: `pip install 'oapi-profile-builder[pdf]'`. Field-by-field detail is in the [`document_metadata`](#document_metadata) reference below; [`examples/nwp_radar.yaml`](examples/nwp_radar.yaml) is a complete DGIWG-branded example.
|
|
247
|
+
|
|
248
|
+
> The `suppress_flavor_logo` override reaches into the OGC Metanorma flavor's XSL (template `insertLogoPreface`) and is coupled to that flavor. Everything else uses documented Metanorma attributes. For long-term SDO adoption, a dedicated Metanorma "taste"/flavor is the most durable path.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
222
252
|
## Profile Config Reference
|
|
223
253
|
|
|
224
254
|
A profile config is a YAML file. The full JSON Schema is at [`profile.schema.json`](profile.schema.json).
|
|
@@ -445,6 +475,61 @@ extent_requirements:
|
|
|
445
475
|
|
|
446
476
|
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.
|
|
447
477
|
|
|
478
|
+
#### Vertical direction (`positive`)
|
|
479
|
+
|
|
480
|
+
OGC API - EDR's vertical extent has no field for which way values increase. This is ambiguous for VRS like pressure levels, which different providers order top-to-bottom or bottom-to-top inconsistently (pressure increases as altitude *decreases*). Set `extent.vertical.positive` to `up` or `down` on a collection (following the CF Conventions `positive` attribute) to make the ordering explicit:
|
|
481
|
+
|
|
482
|
+
```yaml
|
|
483
|
+
collections:
|
|
484
|
+
- id: my_collection
|
|
485
|
+
extent:
|
|
486
|
+
spatial: { ... }
|
|
487
|
+
vertical:
|
|
488
|
+
interval:
|
|
489
|
+
- ["1000", "100"] # hPa, surface to top of atmosphere
|
|
490
|
+
values: ["1000", "850", "700", "500", "300", "100"]
|
|
491
|
+
vrs: "http://www.opengis.net/def/crs/EPSG/0/5798" # or similar pressure VRS
|
|
492
|
+
positive: down # pressure increases downward, toward the surface
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Set `extent_requirements.require_vertical_direction: true` to make `positive` mandatory on every collection with a vertical extent, so a profile can enforce one consistent convention rather than leaving it up to each collection author:
|
|
496
|
+
|
|
497
|
+
```yaml
|
|
498
|
+
extent_requirements:
|
|
499
|
+
minimum_bbox: [-180, -90, 180, 90]
|
|
500
|
+
extent_crs:
|
|
501
|
+
allowed: ["http://www.opengis.net/def/crs/OGC/1.3/CRS84"]
|
|
502
|
+
require_vertical_direction: true
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
### `output_formats`
|
|
508
|
+
|
|
509
|
+
Maps format names (referenced by `collections[].output_formats` and `data_queries.*.variables.output_formats`) to a media type and, optionally, a schema reference. The `schema_ref` flows into the generated OpenAPI response `content` for that format — use it to point a format at a specific schema (e.g. a DGIWG-published schema for GeoTIFF or CoverageJSON) instead of the tool's built-in default.
|
|
510
|
+
|
|
511
|
+
```yaml
|
|
512
|
+
output_formats:
|
|
513
|
+
- name: CoverageJSON
|
|
514
|
+
media_type: application/prs.coverage+json
|
|
515
|
+
schema_ref: https://schemas.opengis.net/covjson/1.0/coveragejson.json
|
|
516
|
+
- name: GeoJSON
|
|
517
|
+
media_type: application/geo+json
|
|
518
|
+
schema_ref: https://geojson.org/schema/FeatureCollection.json
|
|
519
|
+
- name: GeoTIFF
|
|
520
|
+
media_type: image/tiff; application=geotiff
|
|
521
|
+
# Omit schema_ref until a published schema exists for this format —
|
|
522
|
+
# the requirement/abstract test can still state the standard to conform to.
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
| Field | Required | Description |
|
|
526
|
+
|---|---|---|
|
|
527
|
+
| `name` | yes | Format name, referenced elsewhere as a string (e.g. `CoverageJSON`) |
|
|
528
|
+
| `media_type` | yes | MIME type used as the OpenAPI response `content` key and the `f` parameter value |
|
|
529
|
+
| `schema_ref` | no | URL to a JSON Schema (or other schema) for this format. When set, the generated response uses `{"$ref": schema_ref}`; when omitted, a generic/default schema is used |
|
|
530
|
+
|
|
531
|
+
`schema_ref` is optional — if no schema exists yet for a format (as is currently the case for some binary formats like GeoTIFF), leave it unset and capture the required standard as a normative `requirements[]` entry instead. Add `schema_ref` later once a schema URL exists.
|
|
532
|
+
|
|
448
533
|
---
|
|
449
534
|
|
|
450
535
|
### `parameter_schema`
|
|
@@ -601,6 +686,55 @@ document_metadata:
|
|
|
601
686
|
|
|
602
687
|
The cover-page sub-title (e.g. "Draft Standard — Implementation — Approved") is produced by Metanorma from the `doc_type`, `doc_subtype`, and `status` combination.
|
|
603
688
|
|
|
689
|
+
#### Custom branding (logo + colors)
|
|
690
|
+
|
|
691
|
+
Two `document_metadata` blocks let you rebrand the PDF (e.g. for DGIWG) without a custom Metanorma flavor:
|
|
692
|
+
|
|
693
|
+
- **`colors`** — recolours the PDF via Metanorma's `:presentation-metadata-color-*:` attributes. Values are hex colours.
|
|
694
|
+
- **`cover`** — replaces the built-in OGC cover page with a generated one: your logo plus the document's title, number, edition and date. Requires Pillow (`pip install 'oapi-profile-builder[pdf]'`). The builder renders `cover.png` into the output directory and points Metanorma at it via `:coverpage-image:` + `:presentation-metadata-full-coverpage-replacement:`.
|
|
695
|
+
|
|
696
|
+
```yaml
|
|
697
|
+
document_metadata:
|
|
698
|
+
doc_number: "DGIWG 134"
|
|
699
|
+
cover:
|
|
700
|
+
logo: assets/dgiwg_logo.png # path relative to the working directory (or absolute)
|
|
701
|
+
tagline: Delivering Military Advantage through multi-national geospatial interoperability
|
|
702
|
+
background: "#FFFFFF" # optional cover background (default white)
|
|
703
|
+
text_color: "#1F3864" # optional cover text colour (default black)
|
|
704
|
+
colors:
|
|
705
|
+
text: "#000000" # body text
|
|
706
|
+
cover_text: "#1F3864" # cover text / section numbers / ToC
|
|
707
|
+
cover_lines: "#1F3864" # preface 'crossing lines' element
|
|
708
|
+
title: "#1F3864" # clause/table/figure titles
|
|
709
|
+
page_background: "#FFFFFF" # cover + section page background
|
|
710
|
+
table_header: "#1F3864" # table header background
|
|
711
|
+
table_row_even: "#EEF1F7" # even table rows
|
|
712
|
+
table_row_odd: "#FFFFFF" # odd table rows
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
Note: colours work against the normal OGC cover too. The `cover` block does a *full* cover replacement, so the dynamic title/number/edition/date are drawn onto the generated image (which is why the builder composes it rather than relying on Metanorma attributes — logo attributes like `:coverpage-image:` alone are not rendered by the OGC flavor).
|
|
716
|
+
|
|
717
|
+
#### De-branding the rest of the document (legal page + footer + logo)
|
|
718
|
+
|
|
719
|
+
Cover + colors handle the front page, but the OGC flavor also stamps its identity on the page-ii legal boilerplate, the page footer, and an OGC logo on the legal page. Three more `document_metadata` fields remove those:
|
|
720
|
+
|
|
721
|
+
- **`copyright_holder`** — sets `:copyright-holder:`, which also drives the **PDF footer** organisation name (replacing "OPEN GEOSPATIAL CONSORTIUM").
|
|
722
|
+
- **`boilerplate`** — replaces the page-ii legal text (copyright / license / legal notice / feedback) via `:boilerplate-authority:`. Any field left unset is synthesised from `copyright_holder`, so no OGC text leaks through.
|
|
723
|
+
- **`suppress_flavor_logo`** — removes the OGC logo from the preface/legal page via a targeted PDF-stylesheet override. (Coupled to the OGC Metanorma flavor's XSL.)
|
|
724
|
+
|
|
725
|
+
```yaml
|
|
726
|
+
document_metadata:
|
|
727
|
+
copyright_holder: DGIWG
|
|
728
|
+
suppress_flavor_logo: true
|
|
729
|
+
boilerplate:
|
|
730
|
+
copyright: "Copyright © 2026 Defence Geospatial Information Working Group (DGIWG). All rights reserved."
|
|
731
|
+
license: "Use of this document is subject to the DGIWG terms and conditions."
|
|
732
|
+
legal: "Attention is drawn to the possibility that some elements may be subject to patent rights; DGIWG shall not be held responsible for identifying any such rights."
|
|
733
|
+
feedback: "Comments on this document should be directed to DGIWG."
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
With `cover` + `colors` + `copyright_holder` + `boilerplate` + `suppress_flavor_logo` (and `spec_uri_base` for the requirement/conformance identifiers), the generated PDF carries no OGC branding — only legitimate citations to the OGC standards the profile conforms to. See [`examples/nwp_radar.yaml`](examples/nwp_radar.yaml).
|
|
737
|
+
|
|
604
738
|
---
|
|
605
739
|
|
|
606
740
|
### `provider`, `classification`, and provenance metadata
|
|
@@ -672,6 +806,7 @@ The tool enforces these rules at build time. Violations produce clear error mess
|
|
|
672
806
|
| Collection TRS validated | Each `extent.temporal.trs` checked against `extent_trs` |
|
|
673
807
|
| `crs_details` / `crs` validated | Each `data_queries.*.variables.crs_details[].crs` and `crs[]` checked against `supported_crs` |
|
|
674
808
|
| `default_output_format` validated | Each `data_queries.*.variables.default_output_format` must be one of that query's `output_formats` |
|
|
809
|
+
| `require_vertical_direction` enforced | If set, every collection with a vertical extent must declare `extent.vertical.positive` as `up` or `down` |
|
|
675
810
|
| Document dates format | `submission_date` / `approval_date` / `publication_date` must be `YYYY-MM-DD` |
|
|
676
811
|
| Parameters need `unit` + `observedProperty` | Required by OGC API - EDR Part 3 |
|
|
677
812
|
| `parameter_name_pattern` enforced | All `parameter_names` keys must match if set |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OGC API Service Profile Builder
|
|
2
2
|
|
|
3
|
-
Generate OGC API - EDR Part 3 Service Profile artifacts from a YAML config — OpenAPI 3.1.0, AsyncAPI, AsciiDoc requirements
|
|
3
|
+
Generate OGC API - EDR Part 3 Service Profile artifacts from a YAML config — OpenAPI 3.1.0, AsyncAPI, AsciiDoc requirements and conformance tests, and a publication-ready Metanorma PDF with optional custom (e.g. DGIWG) branding.
|
|
4
4
|
|
|
5
5
|
[](https://pypi.org/project/oapi-profile-builder/)
|
|
6
6
|
[](LICENSE)
|
|
@@ -35,13 +35,16 @@ output/
|
|
|
35
35
|
|
|
36
36
|
## Example Profiles
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
Six working examples are included:
|
|
39
39
|
|
|
40
40
|
| File | What it shows |
|
|
41
41
|
|---|---|
|
|
42
42
|
| [`examples/minimal_profile.yaml`](examples/minimal_profile.yaml) | Smallest valid profile — one collection, one requirement |
|
|
43
43
|
| [`examples/insitu_observations_profile.yaml`](examples/insitu_observations_profile.yaml) | Full meteorological profile — 8 parameters with QUDT units, CF standard names, metocean extensions, CRS listing, temporal extent, custom dimensions, `parameter_schema` |
|
|
44
|
+
| [`examples/nws_connect_profile.yaml`](examples/nws_connect_profile.yaml) | PubSub profile — generates `asyncapi.yaml` alongside the OpenAPI |
|
|
44
45
|
| [`examples/nwsviz_profile.yaml`](examples/nwsviz_profile.yaml) | Production profile — 13 collections, 3 OGC API Processes, PDF metadata |
|
|
46
|
+
| [`examples/nwp_radar.yaml`](examples/nwp_radar.yaml) | **DGIWG-branded** profile — custom cover logo, DGIWG colours/footer/legal page, `default_output_format`, per-query CRS, vertical `positive`, DGIWG namespace |
|
|
47
|
+
| [`examples/nwp_earth_observations_lightning_profile.yaml`](examples/nwp_earth_observations_lightning_profile.yaml) | DGIWG lightning profile — same branding, CF units |
|
|
45
48
|
|
|
46
49
|
---
|
|
47
50
|
|
|
@@ -70,6 +73,8 @@ Add `--pdf` to also compile an OGC-compliant PDF via the `metanorma/metanorma` D
|
|
|
70
73
|
oapi-profile-builder generate --config my_profile.yaml --output ./output --pdf
|
|
71
74
|
```
|
|
72
75
|
|
|
76
|
+
The PDF can be rebranded for another SDO (e.g. DGIWG) — custom cover logo, colours, legal page, footer, and identifier namespace — all from the profile config. See [Document generation and branding](#document-generation-and-branding).
|
|
77
|
+
|
|
73
78
|
### `validate`
|
|
74
79
|
|
|
75
80
|
Validates the config without writing any files. Useful in CI before generating.
|
|
@@ -174,6 +179,29 @@ jobs:
|
|
|
174
179
|
|
|
175
180
|
---
|
|
176
181
|
|
|
182
|
+
## Document generation and branding
|
|
183
|
+
|
|
184
|
+
With `--pdf`, the builder compiles a Metanorma document (`document.adoc` + `sections/`, `requirements/`, `abstract_tests/`) into `document.pdf` using the official `metanorma/metanorma` Docker image. By default this is an OGC `draft-standard` document.
|
|
185
|
+
|
|
186
|
+
Everything about the document — cover, title-page metadata, legal page, footer, colours, and even the requirement/conformance identifier namespace — is driven from the profile config, so the same tooling can produce a document branded for another SDO such as **DGIWG** with no OGC branding remaining.
|
|
187
|
+
|
|
188
|
+
| What you want to change | Field | Mechanism |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| Cover logo + tagline (replaces the OGC cover) | `document_metadata.cover` | Generated cover image via `:coverpage-image:` full replacement |
|
|
191
|
+
| Title, doc number, edition, dates, sub-title/status | `title`, `version`, `document_metadata.{doc_number, doc_type, doc_subtype, status, *_date}` | Metanorma document attributes |
|
|
192
|
+
| PDF colours (titles, tables, cover, backgrounds) | `document_metadata.colors` | `:presentation-metadata-color-*:` |
|
|
193
|
+
| Page-ii legal text (copyright / license / notice) | `document_metadata.boilerplate` | `:boilerplate-authority:` |
|
|
194
|
+
| Footer organisation name | `document_metadata.copyright_holder` | `:copyright-holder:` |
|
|
195
|
+
| Remove the OGC logo from the legal page | `document_metadata.suppress_flavor_logo` | targeted `:pdf-stylesheet-override:` |
|
|
196
|
+
| Requirement / conformance identifier namespace | `spec_uri_base` | derives `req_uri` / `conf_uri` |
|
|
197
|
+
| Submitter table, notice paragraph, normative refs | `document_metadata.{submitters, notice, normative_references}` | Metanorma attributes / sections |
|
|
198
|
+
|
|
199
|
+
Custom branding requires Pillow for the cover image: `pip install 'oapi-profile-builder[pdf]'`. Field-by-field detail is in the [`document_metadata`](#document_metadata) reference below; [`examples/nwp_radar.yaml`](examples/nwp_radar.yaml) is a complete DGIWG-branded example.
|
|
200
|
+
|
|
201
|
+
> The `suppress_flavor_logo` override reaches into the OGC Metanorma flavor's XSL (template `insertLogoPreface`) and is coupled to that flavor. Everything else uses documented Metanorma attributes. For long-term SDO adoption, a dedicated Metanorma "taste"/flavor is the most durable path.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
177
205
|
## Profile Config Reference
|
|
178
206
|
|
|
179
207
|
A profile config is a YAML file. The full JSON Schema is at [`profile.schema.json`](profile.schema.json).
|
|
@@ -400,6 +428,61 @@ extent_requirements:
|
|
|
400
428
|
|
|
401
429
|
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.
|
|
402
430
|
|
|
431
|
+
#### Vertical direction (`positive`)
|
|
432
|
+
|
|
433
|
+
OGC API - EDR's vertical extent has no field for which way values increase. This is ambiguous for VRS like pressure levels, which different providers order top-to-bottom or bottom-to-top inconsistently (pressure increases as altitude *decreases*). Set `extent.vertical.positive` to `up` or `down` on a collection (following the CF Conventions `positive` attribute) to make the ordering explicit:
|
|
434
|
+
|
|
435
|
+
```yaml
|
|
436
|
+
collections:
|
|
437
|
+
- id: my_collection
|
|
438
|
+
extent:
|
|
439
|
+
spatial: { ... }
|
|
440
|
+
vertical:
|
|
441
|
+
interval:
|
|
442
|
+
- ["1000", "100"] # hPa, surface to top of atmosphere
|
|
443
|
+
values: ["1000", "850", "700", "500", "300", "100"]
|
|
444
|
+
vrs: "http://www.opengis.net/def/crs/EPSG/0/5798" # or similar pressure VRS
|
|
445
|
+
positive: down # pressure increases downward, toward the surface
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Set `extent_requirements.require_vertical_direction: true` to make `positive` mandatory on every collection with a vertical extent, so a profile can enforce one consistent convention rather than leaving it up to each collection author:
|
|
449
|
+
|
|
450
|
+
```yaml
|
|
451
|
+
extent_requirements:
|
|
452
|
+
minimum_bbox: [-180, -90, 180, 90]
|
|
453
|
+
extent_crs:
|
|
454
|
+
allowed: ["http://www.opengis.net/def/crs/OGC/1.3/CRS84"]
|
|
455
|
+
require_vertical_direction: true
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
### `output_formats`
|
|
461
|
+
|
|
462
|
+
Maps format names (referenced by `collections[].output_formats` and `data_queries.*.variables.output_formats`) to a media type and, optionally, a schema reference. The `schema_ref` flows into the generated OpenAPI response `content` for that format — use it to point a format at a specific schema (e.g. a DGIWG-published schema for GeoTIFF or CoverageJSON) instead of the tool's built-in default.
|
|
463
|
+
|
|
464
|
+
```yaml
|
|
465
|
+
output_formats:
|
|
466
|
+
- name: CoverageJSON
|
|
467
|
+
media_type: application/prs.coverage+json
|
|
468
|
+
schema_ref: https://schemas.opengis.net/covjson/1.0/coveragejson.json
|
|
469
|
+
- name: GeoJSON
|
|
470
|
+
media_type: application/geo+json
|
|
471
|
+
schema_ref: https://geojson.org/schema/FeatureCollection.json
|
|
472
|
+
- name: GeoTIFF
|
|
473
|
+
media_type: image/tiff; application=geotiff
|
|
474
|
+
# Omit schema_ref until a published schema exists for this format —
|
|
475
|
+
# the requirement/abstract test can still state the standard to conform to.
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
| Field | Required | Description |
|
|
479
|
+
|---|---|---|
|
|
480
|
+
| `name` | yes | Format name, referenced elsewhere as a string (e.g. `CoverageJSON`) |
|
|
481
|
+
| `media_type` | yes | MIME type used as the OpenAPI response `content` key and the `f` parameter value |
|
|
482
|
+
| `schema_ref` | no | URL to a JSON Schema (or other schema) for this format. When set, the generated response uses `{"$ref": schema_ref}`; when omitted, a generic/default schema is used |
|
|
483
|
+
|
|
484
|
+
`schema_ref` is optional — if no schema exists yet for a format (as is currently the case for some binary formats like GeoTIFF), leave it unset and capture the required standard as a normative `requirements[]` entry instead. Add `schema_ref` later once a schema URL exists.
|
|
485
|
+
|
|
403
486
|
---
|
|
404
487
|
|
|
405
488
|
### `parameter_schema`
|
|
@@ -556,6 +639,55 @@ document_metadata:
|
|
|
556
639
|
|
|
557
640
|
The cover-page sub-title (e.g. "Draft Standard — Implementation — Approved") is produced by Metanorma from the `doc_type`, `doc_subtype`, and `status` combination.
|
|
558
641
|
|
|
642
|
+
#### Custom branding (logo + colors)
|
|
643
|
+
|
|
644
|
+
Two `document_metadata` blocks let you rebrand the PDF (e.g. for DGIWG) without a custom Metanorma flavor:
|
|
645
|
+
|
|
646
|
+
- **`colors`** — recolours the PDF via Metanorma's `:presentation-metadata-color-*:` attributes. Values are hex colours.
|
|
647
|
+
- **`cover`** — replaces the built-in OGC cover page with a generated one: your logo plus the document's title, number, edition and date. Requires Pillow (`pip install 'oapi-profile-builder[pdf]'`). The builder renders `cover.png` into the output directory and points Metanorma at it via `:coverpage-image:` + `:presentation-metadata-full-coverpage-replacement:`.
|
|
648
|
+
|
|
649
|
+
```yaml
|
|
650
|
+
document_metadata:
|
|
651
|
+
doc_number: "DGIWG 134"
|
|
652
|
+
cover:
|
|
653
|
+
logo: assets/dgiwg_logo.png # path relative to the working directory (or absolute)
|
|
654
|
+
tagline: Delivering Military Advantage through multi-national geospatial interoperability
|
|
655
|
+
background: "#FFFFFF" # optional cover background (default white)
|
|
656
|
+
text_color: "#1F3864" # optional cover text colour (default black)
|
|
657
|
+
colors:
|
|
658
|
+
text: "#000000" # body text
|
|
659
|
+
cover_text: "#1F3864" # cover text / section numbers / ToC
|
|
660
|
+
cover_lines: "#1F3864" # preface 'crossing lines' element
|
|
661
|
+
title: "#1F3864" # clause/table/figure titles
|
|
662
|
+
page_background: "#FFFFFF" # cover + section page background
|
|
663
|
+
table_header: "#1F3864" # table header background
|
|
664
|
+
table_row_even: "#EEF1F7" # even table rows
|
|
665
|
+
table_row_odd: "#FFFFFF" # odd table rows
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
Note: colours work against the normal OGC cover too. The `cover` block does a *full* cover replacement, so the dynamic title/number/edition/date are drawn onto the generated image (which is why the builder composes it rather than relying on Metanorma attributes — logo attributes like `:coverpage-image:` alone are not rendered by the OGC flavor).
|
|
669
|
+
|
|
670
|
+
#### De-branding the rest of the document (legal page + footer + logo)
|
|
671
|
+
|
|
672
|
+
Cover + colors handle the front page, but the OGC flavor also stamps its identity on the page-ii legal boilerplate, the page footer, and an OGC logo on the legal page. Three more `document_metadata` fields remove those:
|
|
673
|
+
|
|
674
|
+
- **`copyright_holder`** — sets `:copyright-holder:`, which also drives the **PDF footer** organisation name (replacing "OPEN GEOSPATIAL CONSORTIUM").
|
|
675
|
+
- **`boilerplate`** — replaces the page-ii legal text (copyright / license / legal notice / feedback) via `:boilerplate-authority:`. Any field left unset is synthesised from `copyright_holder`, so no OGC text leaks through.
|
|
676
|
+
- **`suppress_flavor_logo`** — removes the OGC logo from the preface/legal page via a targeted PDF-stylesheet override. (Coupled to the OGC Metanorma flavor's XSL.)
|
|
677
|
+
|
|
678
|
+
```yaml
|
|
679
|
+
document_metadata:
|
|
680
|
+
copyright_holder: DGIWG
|
|
681
|
+
suppress_flavor_logo: true
|
|
682
|
+
boilerplate:
|
|
683
|
+
copyright: "Copyright © 2026 Defence Geospatial Information Working Group (DGIWG). All rights reserved."
|
|
684
|
+
license: "Use of this document is subject to the DGIWG terms and conditions."
|
|
685
|
+
legal: "Attention is drawn to the possibility that some elements may be subject to patent rights; DGIWG shall not be held responsible for identifying any such rights."
|
|
686
|
+
feedback: "Comments on this document should be directed to DGIWG."
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
With `cover` + `colors` + `copyright_holder` + `boilerplate` + `suppress_flavor_logo` (and `spec_uri_base` for the requirement/conformance identifiers), the generated PDF carries no OGC branding — only legitimate citations to the OGC standards the profile conforms to. See [`examples/nwp_radar.yaml`](examples/nwp_radar.yaml).
|
|
690
|
+
|
|
559
691
|
---
|
|
560
692
|
|
|
561
693
|
### `provider`, `classification`, and provenance metadata
|
|
@@ -627,6 +759,7 @@ The tool enforces these rules at build time. Violations produce clear error mess
|
|
|
627
759
|
| Collection TRS validated | Each `extent.temporal.trs` checked against `extent_trs` |
|
|
628
760
|
| `crs_details` / `crs` validated | Each `data_queries.*.variables.crs_details[].crs` and `crs[]` checked against `supported_crs` |
|
|
629
761
|
| `default_output_format` validated | Each `data_queries.*.variables.default_output_format` must be one of that query's `output_formats` |
|
|
762
|
+
| `require_vertical_direction` enforced | If set, every collection with a vertical extent must declare `extent.vertical.positive` as `up` or `down` |
|
|
630
763
|
| Document dates format | `submission_date` / `approval_date` / `publication_date` must be `YYYY-MM-DD` |
|
|
631
764
|
| Parameters need `unit` + `observedProperty` | Required by OGC API - EDR Part 3 |
|
|
632
765
|
| `parameter_name_pattern` enforced | All `parameter_names` keys must match if set |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "oapi-profile-builder"
|
|
7
|
-
version = "3.1
|
|
7
|
+
version = "3.2.1"
|
|
8
8
|
description = "Authoritative tooling for creating OGC API Service Profiles (EDR, Features)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -30,6 +30,7 @@ dependencies = [
|
|
|
30
30
|
|
|
31
31
|
[project.optional-dependencies]
|
|
32
32
|
validate = ["schemathesis>=3.0"]
|
|
33
|
+
pdf = ["pillow>=10.0"]
|
|
33
34
|
|
|
34
35
|
[project.urls]
|
|
35
36
|
Homepage = "https://github.com/ShaneMill1/OGC-API-Service-Profile-Builder"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# =================================================================
|
|
2
|
+
#
|
|
3
|
+
# Authors: Shane Mill <shane.mill@noaa.gov>
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2026 Shane Mill
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person
|
|
8
|
+
# obtaining a copy of this software and associated documentation
|
|
9
|
+
# files (the "Software"), to deal in the Software without
|
|
10
|
+
# restriction, including without limitation the rights to use,
|
|
11
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
# copies of the Software, and to permit persons to whom the
|
|
13
|
+
# Software is furnished to do so, subject to the following
|
|
14
|
+
# conditions:
|
|
15
|
+
#
|
|
16
|
+
# The above copyright notice and this permission notice shall be
|
|
17
|
+
# included in all copies or substantial portions of the Software.
|
|
18
|
+
#
|
|
19
|
+
# =================================================================
|
|
20
|
+
"""
|
|
21
|
+
Custom PDF cover-page generation.
|
|
22
|
+
|
|
23
|
+
When a profile sets ``document_metadata.cover.logo``, the builder renders a
|
|
24
|
+
replacement cover-page image (organisation logo + the document's title, number,
|
|
25
|
+
edition and date) which Metanorma then uses in place of the built-in OGC cover
|
|
26
|
+
via ``:coverpage-image:`` + ``:presentation-metadata-full-coverpage-replacement:``.
|
|
27
|
+
|
|
28
|
+
Rendering uses Pillow. DejaVu fonts are bundled with Pillow and resolved by name,
|
|
29
|
+
so no system font paths are required.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from datetime import datetime
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
# A4 portrait at ~150 DPI.
|
|
38
|
+
_PAGE_W = 1240
|
|
39
|
+
_PAGE_H = 1754
|
|
40
|
+
|
|
41
|
+
_COVER_FILENAME = "cover.png"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _load_font(bold: bool, size: int):
|
|
45
|
+
from PIL import ImageFont
|
|
46
|
+
|
|
47
|
+
name = "DejaVuSans-Bold.ttf" if bold else "DejaVuSans.ttf"
|
|
48
|
+
try:
|
|
49
|
+
return ImageFont.truetype(name, size)
|
|
50
|
+
except Exception:
|
|
51
|
+
# Last-resort fallback; not scalable but keeps rendering from failing.
|
|
52
|
+
return ImageFont.load_default()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _italic_font(size: int):
|
|
56
|
+
from PIL import ImageFont
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
return ImageFont.truetype("DejaVuSans-Oblique.ttf", size)
|
|
60
|
+
except Exception:
|
|
61
|
+
return _load_font(False, size)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _format_date(iso_date: str | None) -> str | None:
|
|
65
|
+
"""Format 'YYYY-MM-DD' as 'DD Month YYYY' (e.g. '15 May 2026')."""
|
|
66
|
+
if not iso_date:
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
dt = datetime.strptime(iso_date, "%Y-%m-%d")
|
|
70
|
+
except ValueError:
|
|
71
|
+
return iso_date
|
|
72
|
+
# %-d is not portable; strip a leading zero manually.
|
|
73
|
+
return f"{dt.day} {dt.strftime('%B %Y')}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_cover_image(profile, output_dir: Path) -> str | None:
|
|
77
|
+
"""Render the cover PNG into output_dir. Returns its filename, or None.
|
|
78
|
+
|
|
79
|
+
Returns None when no cover logo is configured. Raises RuntimeError with a
|
|
80
|
+
clear message if Pillow is unavailable or the logo file cannot be found.
|
|
81
|
+
"""
|
|
82
|
+
m = getattr(profile, "document_metadata", None)
|
|
83
|
+
cover = getattr(m, "cover", None) if m else None
|
|
84
|
+
if not cover or not cover.logo:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
from PIL import Image, ImageDraw
|
|
89
|
+
except ImportError as exc: # pragma: no cover - depends on env
|
|
90
|
+
raise RuntimeError(
|
|
91
|
+
"document_metadata.cover.logo is set but Pillow is not installed. "
|
|
92
|
+
"Install it with: pip install 'oapi-profile-builder[pdf]'"
|
|
93
|
+
) from exc
|
|
94
|
+
|
|
95
|
+
logo_path = Path(cover.logo).expanduser()
|
|
96
|
+
if not logo_path.is_absolute():
|
|
97
|
+
logo_path = (Path.cwd() / logo_path).resolve()
|
|
98
|
+
if not logo_path.exists():
|
|
99
|
+
raise RuntimeError(f"Cover logo not found: {logo_path}")
|
|
100
|
+
|
|
101
|
+
bg = cover.background or "#FFFFFF"
|
|
102
|
+
fg = cover.text_color or "#000000"
|
|
103
|
+
|
|
104
|
+
page = Image.new("RGB", (_PAGE_W, _PAGE_H), bg)
|
|
105
|
+
draw = ImageDraw.Draw(page)
|
|
106
|
+
|
|
107
|
+
def centered(text: str, font, y: int, fill: str) -> int:
|
|
108
|
+
"""Draw text horizontally centered; return the y below the text."""
|
|
109
|
+
bbox = draw.textbbox((0, 0), text, font=font)
|
|
110
|
+
tw = bbox[2] - bbox[0]
|
|
111
|
+
th = bbox[3] - bbox[1]
|
|
112
|
+
draw.text(((_PAGE_W - tw) / 2, y), text, font=font, fill=fill)
|
|
113
|
+
return y + th
|
|
114
|
+
|
|
115
|
+
y = 180
|
|
116
|
+
|
|
117
|
+
# --- Logo (centered, scaled to a target width) ---
|
|
118
|
+
logo = Image.open(logo_path).convert("RGBA")
|
|
119
|
+
target_logo_w = 420 # desired cover logo width
|
|
120
|
+
max_logo_w = 520 # hard cap
|
|
121
|
+
max_upscale = 3.0 # don't upscale tiny logos beyond this (avoids heavy blur)
|
|
122
|
+
desired = min(target_logo_w, int(logo.width * max_upscale), max_logo_w)
|
|
123
|
+
if logo.width != desired:
|
|
124
|
+
scale = desired / logo.width
|
|
125
|
+
logo = logo.resize((desired, max(1, int(logo.height * scale))))
|
|
126
|
+
logo_x = int((_PAGE_W - logo.width) / 2)
|
|
127
|
+
page.paste(logo, (logo_x, y), logo)
|
|
128
|
+
y += logo.height + 60
|
|
129
|
+
|
|
130
|
+
# --- Tagline (italic) ---
|
|
131
|
+
if cover.tagline:
|
|
132
|
+
y = centered(cover.tagline, _italic_font(30), y, fg) + 60
|
|
133
|
+
|
|
134
|
+
# --- Document number ---
|
|
135
|
+
if m and m.doc_number:
|
|
136
|
+
y = centered(m.doc_number, _load_font(True, 46), y, fg) + 40
|
|
137
|
+
|
|
138
|
+
# --- Edition ---
|
|
139
|
+
edition = getattr(profile, "version", None)
|
|
140
|
+
if edition:
|
|
141
|
+
y = centered(f"Edition {edition}", _load_font(False, 28), y, fg) + 24
|
|
142
|
+
|
|
143
|
+
# --- Date ---
|
|
144
|
+
date_str = _format_date(m.publication_date if m else None)
|
|
145
|
+
if date_str:
|
|
146
|
+
y = centered(f"Dated {date_str}", _load_font(False, 28), y, fg) + 80
|
|
147
|
+
|
|
148
|
+
# --- Title (large, bold, wrapped) ---
|
|
149
|
+
title = getattr(profile, "title", "") or ""
|
|
150
|
+
title_font = _load_font(True, 54)
|
|
151
|
+
for line in _wrap(draw, title, title_font, _PAGE_W - 240):
|
|
152
|
+
y = centered(line, title_font, y, fg) + 16
|
|
153
|
+
|
|
154
|
+
target = (output_dir / _COVER_FILENAME).resolve()
|
|
155
|
+
if not str(target).startswith(str(output_dir.resolve())):
|
|
156
|
+
raise RuntimeError(f"Refusing to write cover outside output directory: {target}")
|
|
157
|
+
page.save(target)
|
|
158
|
+
return _COVER_FILENAME
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _wrap(draw, text: str, font, max_width: int) -> list[str]:
|
|
162
|
+
"""Greedy word-wrap to fit max_width pixels."""
|
|
163
|
+
words = text.split()
|
|
164
|
+
lines: list[str] = []
|
|
165
|
+
cur = ""
|
|
166
|
+
for w in words:
|
|
167
|
+
trial = f"{cur} {w}".strip()
|
|
168
|
+
bbox = draw.textbbox((0, 0), trial, font=font)
|
|
169
|
+
if bbox[2] - bbox[0] <= max_width or not cur:
|
|
170
|
+
cur = trial
|
|
171
|
+
else:
|
|
172
|
+
lines.append(cur)
|
|
173
|
+
cur = w
|
|
174
|
+
if cur:
|
|
175
|
+
lines.append(cur)
|
|
176
|
+
return lines
|