oapi-profile-builder 3.1.0__tar.gz → 3.2.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-3.1.0/src/oapi_profile_builder.egg-info → oapi_profile_builder-3.2.0}/PKG-INFO +138 -3
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/README.md +135 -2
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/pyproject.toml +2 -1
- oapi_profile_builder-3.2.0/src/oapi_profile_builder/cover.py +176 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/generate.py +120 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/models.py +165 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0/src/oapi_profile_builder.egg-info}/PKG-INFO +138 -3
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder.egg-info/SOURCES.txt +1 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder.egg-info/requires.txt +3 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/LICENSE +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/setup.cfg +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/__init__.py +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/cite.py +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/cite_features.py +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/cli.py +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/compile.py +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/server_validation.py +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder.egg-info/dependency_links.txt +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder.egg-info/entry_points.txt +0 -0
- {oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/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.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oapi-profile-builder
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2.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
|
|
@@ -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.
|
|
7
|
+
version = "3.2.0"
|
|
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
|
{oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/generate.py
RENAMED
|
@@ -552,6 +552,15 @@ def _collection_response_schema(coll: Collection,
|
|
|
552
552
|
"items": {"type": "string"},
|
|
553
553
|
},
|
|
554
554
|
"vrs": vrs_schema,
|
|
555
|
+
"positive": {
|
|
556
|
+
"type": "string",
|
|
557
|
+
"enum": ["up", "down"],
|
|
558
|
+
"description": (
|
|
559
|
+
"Direction in which vertical extent values increase "
|
|
560
|
+
"(CF Conventions 'positive' attribute). Disambiguates "
|
|
561
|
+
"interval ordering for VRS such as pressure levels."
|
|
562
|
+
),
|
|
563
|
+
},
|
|
555
564
|
},
|
|
556
565
|
},
|
|
557
566
|
"custom": {
|
|
@@ -1365,6 +1374,82 @@ def _individual_test_adoc(profile: ServiceProfile, test_id: str) -> str:
|
|
|
1365
1374
|
# Metanorma root document
|
|
1366
1375
|
# ---------------------------------------------------------------------------
|
|
1367
1376
|
|
|
1377
|
+
_LOGO_OVERRIDE_XSL = """<?xml version="1.0" encoding="UTF-8"?>
|
|
1378
|
+
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
|
1379
|
+
xmlns:fo="http://www.w3.org/1999/XSL/Format"
|
|
1380
|
+
version="1.0">
|
|
1381
|
+
<!-- Rebranding override: suppress the OGC flavor logo on the preface/legal
|
|
1382
|
+
page (and the cover, which is replaced separately). Coupled to the OGC
|
|
1383
|
+
Metanorma flavor's XSL template names. -->
|
|
1384
|
+
<xsl:template name="insertLogoPreface"/>
|
|
1385
|
+
<xsl:template name="insertLogo"/>
|
|
1386
|
+
</xsl:stylesheet>
|
|
1387
|
+
"""
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
def _xml_escape(text: str) -> str:
|
|
1391
|
+
return (
|
|
1392
|
+
text.replace("&", "&")
|
|
1393
|
+
.replace("<", "<")
|
|
1394
|
+
.replace(">", ">")
|
|
1395
|
+
)
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
def _build_boilerplate_xml(profile: ServiceProfile) -> str | None:
|
|
1399
|
+
"""Build a Metanorma boilerplate XML overriding the flavor's legal text.
|
|
1400
|
+
|
|
1401
|
+
Returns the XML string, or None when no boilerplate is configured. Any
|
|
1402
|
+
statement not supplied is synthesised from copyright_holder so no flavor
|
|
1403
|
+
default (e.g. OGC copyright) leaks through.
|
|
1404
|
+
"""
|
|
1405
|
+
m = profile.document_metadata
|
|
1406
|
+
bp = m.boilerplate if m else None
|
|
1407
|
+
if not bp:
|
|
1408
|
+
return None
|
|
1409
|
+
|
|
1410
|
+
holder = (m.copyright_holder if m and m.copyright_holder else None) or (
|
|
1411
|
+
m.submitting_orgs[0] if m and m.submitting_orgs else "the copyright holder"
|
|
1412
|
+
)
|
|
1413
|
+
year = m.copyright_year if m else 2026
|
|
1414
|
+
|
|
1415
|
+
copyright_txt = bp.copyright or f"Copyright © {year} {holder}. All rights reserved."
|
|
1416
|
+
license_txt = bp.license or f"Use of this document is subject to the terms and conditions of {holder}."
|
|
1417
|
+
legal_txt = bp.legal or (
|
|
1418
|
+
"Attention is drawn to the possibility that some of the elements of this "
|
|
1419
|
+
f"document may be the subject of patent rights. {holder} shall not be held "
|
|
1420
|
+
"responsible for identifying any or all such patent rights."
|
|
1421
|
+
)
|
|
1422
|
+
feedback_txt = bp.feedback or f"Comments on this document should be directed to {holder}."
|
|
1423
|
+
|
|
1424
|
+
return (
|
|
1425
|
+
"<boilerplate>\n"
|
|
1426
|
+
" <copyright-statement>\n"
|
|
1427
|
+
" <clause>\n"
|
|
1428
|
+
" <title>Copyright notice</title>\n"
|
|
1429
|
+
f" <p id=\"bp-copyright\">{_xml_escape(copyright_txt)}</p>\n"
|
|
1430
|
+
" </clause>\n"
|
|
1431
|
+
" </copyright-statement>\n"
|
|
1432
|
+
" <license-statement>\n"
|
|
1433
|
+
" <clause>\n"
|
|
1434
|
+
" <title>License Agreement</title>\n"
|
|
1435
|
+
f" <p id=\"bp-license\">{_xml_escape(license_txt)}</p>\n"
|
|
1436
|
+
" </clause>\n"
|
|
1437
|
+
" </license-statement>\n"
|
|
1438
|
+
" <legal-statement>\n"
|
|
1439
|
+
" <clause>\n"
|
|
1440
|
+
" <title>Note</title>\n"
|
|
1441
|
+
f" <p id=\"bp-legal\">{_xml_escape(legal_txt)}</p>\n"
|
|
1442
|
+
" </clause>\n"
|
|
1443
|
+
" </legal-statement>\n"
|
|
1444
|
+
" <feedback-statement>\n"
|
|
1445
|
+
" <clause>\n"
|
|
1446
|
+
f" <p id=\"bp-feedback\">{_xml_escape(feedback_txt)}</p>\n"
|
|
1447
|
+
" </clause>\n"
|
|
1448
|
+
" </feedback-statement>\n"
|
|
1449
|
+
"</boilerplate>\n"
|
|
1450
|
+
)
|
|
1451
|
+
|
|
1452
|
+
|
|
1368
1453
|
def _build_document_adoc(profile: ServiceProfile) -> str:
|
|
1369
1454
|
m = profile.document_metadata
|
|
1370
1455
|
year = m.copyright_year if m else 2026
|
|
@@ -1400,6 +1485,24 @@ def _build_document_adoc(profile: ServiceProfile) -> str:
|
|
|
1400
1485
|
lines.append(f":keywords: {', '.join(m.keywords)}")
|
|
1401
1486
|
if m and m.submitting_orgs:
|
|
1402
1487
|
lines.append(f":submitting-organizations: {'; '.join(m.submitting_orgs)}")
|
|
1488
|
+
# PDF colour-scheme overrides (mapped to Metanorma attributes).
|
|
1489
|
+
if m and m.colors:
|
|
1490
|
+
for attr, value in m.colors.to_metanorma_attributes().items():
|
|
1491
|
+
lines.append(f":{attr}: {value}")
|
|
1492
|
+
# Custom cover page: replace the built-in OGC cover with our generated image.
|
|
1493
|
+
# The image file (cover.png) is written by generate(); this just references it.
|
|
1494
|
+
if m and m.cover and m.cover.logo:
|
|
1495
|
+
lines.append(":coverpage-image: cover.png")
|
|
1496
|
+
lines.append(":presentation-metadata-full-coverpage-replacement: true")
|
|
1497
|
+
# Copyright holder → also sets the PDF footer org name (replaces OGC default).
|
|
1498
|
+
if m and m.copyright_holder:
|
|
1499
|
+
lines.append(f":copyright-holder: {m.copyright_holder}")
|
|
1500
|
+
# Custom legal boilerplate (page ii). The XML file is written by generate().
|
|
1501
|
+
if m and m.boilerplate:
|
|
1502
|
+
lines.append(":boilerplate-authority: boilerplate.xml")
|
|
1503
|
+
# Suppress the flavor's built-in (OGC) logo on the preface page when rebranding.
|
|
1504
|
+
if m and m.suppress_flavor_logo:
|
|
1505
|
+
lines.append(":pdf-stylesheet-override: logo_override.xsl")
|
|
1403
1506
|
lines += [
|
|
1404
1507
|
":mn-document-class: ogc",
|
|
1405
1508
|
":mn-output-extensions: xml,html,pdf",
|
|
@@ -1622,4 +1725,21 @@ def generate(profile: ServiceProfile, output_dir: Path) -> None:
|
|
|
1622
1725
|
for path, content in _build_sections(profile).items():
|
|
1623
1726
|
safe_write(path, content)
|
|
1624
1727
|
|
|
1728
|
+
# Custom cover-page image (only when document_metadata.cover.logo is set).
|
|
1729
|
+
m = profile.document_metadata
|
|
1730
|
+
if m and m.cover and m.cover.logo:
|
|
1731
|
+
from oapi_profile_builder.cover import build_cover_image
|
|
1732
|
+
cover_file = build_cover_image(profile, output_dir)
|
|
1733
|
+
if cover_file:
|
|
1734
|
+
print(f"Custom cover page written to {output_dir / cover_file}")
|
|
1735
|
+
|
|
1736
|
+
# Custom legal boilerplate (page ii), replacing the flavor's built-in text.
|
|
1737
|
+
boilerplate_xml = _build_boilerplate_xml(profile)
|
|
1738
|
+
if boilerplate_xml:
|
|
1739
|
+
safe_write("boilerplate.xml", boilerplate_xml)
|
|
1740
|
+
|
|
1741
|
+
# PDF stylesheet override to suppress the flavor's preface-page logo.
|
|
1742
|
+
if m and m.suppress_flavor_logo:
|
|
1743
|
+
safe_write("logo_override.xsl", _LOGO_OVERRIDE_XSL)
|
|
1744
|
+
|
|
1625
1745
|
print(f"Profile '{profile.name}' written to {output_dir}")
|
{oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/models.py
RENAMED
|
@@ -73,10 +73,39 @@ class TemporalWithNullBounds(EdrBaseModel):
|
|
|
73
73
|
trs: str
|
|
74
74
|
|
|
75
75
|
|
|
76
|
+
class VerticalWithDirection(Vertical):
|
|
77
|
+
"""Vertical extent with an explicit direction of increasing value.
|
|
78
|
+
|
|
79
|
+
OGC API - EDR's Vertical extent has no way to state whether the interval
|
|
80
|
+
values are ordered "positive up" (e.g. height) or "positive down" (e.g.
|
|
81
|
+
depth, or pressure levels — which increase in magnitude *downward* toward
|
|
82
|
+
the surface while decreasing in altitude). Different data providers order
|
|
83
|
+
pressure-level intervals top-to-bottom or bottom-to-top inconsistently.
|
|
84
|
+
|
|
85
|
+
``positive`` follows the CF Conventions attribute of the same name
|
|
86
|
+
(https://cfconventions.org/cf-conventions/cf-conventions.html#vertical-coordinate)
|
|
87
|
+
so profiles can require a single, unambiguous convention.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
positive: Optional[Literal["up", "down"]] = Field(
|
|
91
|
+
default=None,
|
|
92
|
+
description=(
|
|
93
|
+
"Direction in which vertical extent values increase, per the CF "
|
|
94
|
+
"Conventions 'positive' attribute. 'up' means values increase with "
|
|
95
|
+
"altitude/height (e.g. height above ground); 'down' means values "
|
|
96
|
+
"increase toward the surface/downward (e.g. depth, or pressure — "
|
|
97
|
+
"since pressure increases as altitude decreases). Recommended "
|
|
98
|
+
"whenever the VRS alone doesn't make the ordering unambiguous "
|
|
99
|
+
"(pressure levels are the common case)."
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
76
104
|
class ExtentWithNullBounds(EDRExtent):
|
|
77
105
|
"""Extent that uses TemporalWithNullBounds instead of the upstream Temporal."""
|
|
78
106
|
|
|
79
107
|
temporal: Optional[TemporalWithNullBounds] = None
|
|
108
|
+
vertical: Optional[VerticalWithDirection] = None # type: ignore[assignment]
|
|
80
109
|
|
|
81
110
|
|
|
82
111
|
class Classification(BaseModel):
|
|
@@ -252,6 +281,16 @@ class ExtentRequirements(BaseModel):
|
|
|
252
281
|
default=None,
|
|
253
282
|
description="Constraint on extent.vertical.vrs — the VRS the vertical extent is expressed in",
|
|
254
283
|
)
|
|
284
|
+
require_vertical_direction: bool = Field(
|
|
285
|
+
default=False,
|
|
286
|
+
description=(
|
|
287
|
+
"When true, every collection with a vertical extent must declare "
|
|
288
|
+
"extent.vertical.positive ('up' or 'down'), per the CF Conventions "
|
|
289
|
+
"'positive' attribute. Use this to force a single, unambiguous "
|
|
290
|
+
"ordering convention for vertical intervals (e.g. pressure levels) "
|
|
291
|
+
"across all collections in the profile."
|
|
292
|
+
),
|
|
293
|
+
)
|
|
255
294
|
|
|
256
295
|
# --- Supported CRS/TRS/VRS (lists — what the service supports for queries) ---
|
|
257
296
|
supported_crs: CrsConstraint | None = Field(
|
|
@@ -334,6 +373,91 @@ DocStatus = Literal[
|
|
|
334
373
|
# Loose ISO 8601 date (YYYY-MM-DD) check for document dates.
|
|
335
374
|
_DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
|
|
336
375
|
|
|
376
|
+
# Hex colour (#RGB or #RRGGBB) check for PDF colour overrides.
|
|
377
|
+
_HEX_COLOR_RE = re.compile(r"^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
class PdfColors(BaseModel):
|
|
381
|
+
"""PDF colour-scheme overrides.
|
|
382
|
+
|
|
383
|
+
Each field maps to a Metanorma OGC ``:presentation-metadata-color-*:``
|
|
384
|
+
document attribute, letting a profile recolour the generated PDF (e.g. to a
|
|
385
|
+
DGIWG palette) without a custom Metanorma flavor. Values are hex colours
|
|
386
|
+
(``#RGB`` or ``#RRGGBB``). See
|
|
387
|
+
https://www.metanorma.org/author/ogc/ref/document-attributes/#pdf-color-scheme
|
|
388
|
+
"""
|
|
389
|
+
text: str | None = Field(default=None, description="Body text colour (:presentation-metadata-color-text:)")
|
|
390
|
+
cover_text: str | None = Field(default=None, description="Cover-page text / section numbers / ToC (:presentation-metadata-color-secondary-shade-1:)")
|
|
391
|
+
cover_lines: str | None = Field(default=None, description="Preface 'crossing lines' design element (:presentation-metadata-color-secondary-shade-2:)")
|
|
392
|
+
title: str | None = Field(default=None, description="Clause/table/figure title colour (:presentation-metadata-color-text-title:)")
|
|
393
|
+
page_background: str | None = Field(default=None, description="Cover and section page background (:presentation-metadata-color-background-page:)")
|
|
394
|
+
table_header: str | None = Field(default=None, description="Table header background (:presentation-metadata-color-background-table-header:)")
|
|
395
|
+
table_row_even: str | None = Field(default=None, description="Even table row background (:presentation-metadata-color-background-table-row-even:)")
|
|
396
|
+
table_row_odd: str | None = Field(default=None, description="Odd table row background (:presentation-metadata-color-background-table-row-odd:)")
|
|
397
|
+
|
|
398
|
+
@field_validator("*")
|
|
399
|
+
@classmethod
|
|
400
|
+
def _valid_hex(cls, v: str | None) -> str | None:
|
|
401
|
+
if v is not None and not _HEX_COLOR_RE.match(v):
|
|
402
|
+
raise ValueError(f"colour '{v}' must be a hex colour like #RRGGBB")
|
|
403
|
+
return v
|
|
404
|
+
|
|
405
|
+
def to_metanorma_attributes(self) -> dict[str, str]:
|
|
406
|
+
"""Map set fields to their Metanorma attribute names → value."""
|
|
407
|
+
mapping = {
|
|
408
|
+
"text": "presentation-metadata-color-text",
|
|
409
|
+
"cover_text": "presentation-metadata-color-secondary-shade-1",
|
|
410
|
+
"cover_lines": "presentation-metadata-color-secondary-shade-2",
|
|
411
|
+
"title": "presentation-metadata-color-text-title",
|
|
412
|
+
"page_background": "presentation-metadata-color-background-page",
|
|
413
|
+
"table_header": "presentation-metadata-color-background-table-header",
|
|
414
|
+
"table_row_even": "presentation-metadata-color-background-table-row-even",
|
|
415
|
+
"table_row_odd": "presentation-metadata-color-background-table-row-odd",
|
|
416
|
+
}
|
|
417
|
+
out: dict[str, str] = {}
|
|
418
|
+
for field, attr in mapping.items():
|
|
419
|
+
val = getattr(self, field, None)
|
|
420
|
+
if val:
|
|
421
|
+
out[attr] = val
|
|
422
|
+
return out
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class CoverPage(BaseModel):
|
|
426
|
+
"""Custom PDF cover-page branding (e.g. DGIWG).
|
|
427
|
+
|
|
428
|
+
When ``logo`` is set, the builder renders a replacement cover page (logo +
|
|
429
|
+
the document's title, number, edition and date) and instructs Metanorma to
|
|
430
|
+
use it in place of the built-in OGC cover, via ``:coverpage-image:`` +
|
|
431
|
+
``:presentation-metadata-full-coverpage-replacement:`` (supported for all
|
|
432
|
+
flavors). Requires Pillow (``pip install oapi-profile-builder[pdf]``).
|
|
433
|
+
"""
|
|
434
|
+
logo: str = Field(description="Path to the cover logo image (PNG/JPG), relative to the working directory or absolute")
|
|
435
|
+
tagline: str | None = Field(default=None, description="Optional line rendered under the logo, e.g. an organisation motto")
|
|
436
|
+
background: str | None = Field(default=None, description="Cover background hex colour (default white)")
|
|
437
|
+
text_color: str | None = Field(default=None, description="Cover text hex colour (default black)")
|
|
438
|
+
|
|
439
|
+
@field_validator("background", "text_color")
|
|
440
|
+
@classmethod
|
|
441
|
+
def _valid_hex(cls, v: str | None) -> str | None:
|
|
442
|
+
if v is not None and not _HEX_COLOR_RE.match(v):
|
|
443
|
+
raise ValueError(f"colour '{v}' must be a hex colour like #RRGGBB")
|
|
444
|
+
return v
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
class Boilerplate(BaseModel):
|
|
448
|
+
"""Front-matter legal boilerplate (page ii) — copyright, license, legal notice.
|
|
449
|
+
|
|
450
|
+
Replaces the flavor's built-in boilerplate (e.g. the OGC copyright/license/
|
|
451
|
+
patent notice) via Metanorma's ``:boilerplate-authority:`` mechanism, so a
|
|
452
|
+
DGIWG (or other SDO) document doesn't carry OGC legal text. Any field left
|
|
453
|
+
unset is synthesised from ``copyright_holder`` so no flavor default leaks
|
|
454
|
+
through.
|
|
455
|
+
"""
|
|
456
|
+
copyright: str | None = Field(default=None, description="Copyright notice paragraph")
|
|
457
|
+
license: str | None = Field(default=None, description="License agreement paragraph")
|
|
458
|
+
legal: str | None = Field(default=None, description="Legal/patent notice paragraph")
|
|
459
|
+
feedback: str | None = Field(default=None, description="Feedback/comments paragraph")
|
|
460
|
+
|
|
337
461
|
|
|
338
462
|
class DocumentMetadata(BaseModel):
|
|
339
463
|
"""Metanorma/OGC document header metadata for PDF compilation."""
|
|
@@ -385,6 +509,39 @@ class DocumentMetadata(BaseModel):
|
|
|
385
509
|
description="Additional normative references appended to the References section.",
|
|
386
510
|
)
|
|
387
511
|
external_id: str | None = None
|
|
512
|
+
colors: PdfColors | None = Field(
|
|
513
|
+
default=None,
|
|
514
|
+
description="PDF colour-scheme overrides (mapped to Metanorma :presentation-metadata-color-*: attributes).",
|
|
515
|
+
)
|
|
516
|
+
cover: CoverPage | None = Field(
|
|
517
|
+
default=None,
|
|
518
|
+
description="Custom cover-page branding (logo + generated cover) replacing the built-in OGC cover.",
|
|
519
|
+
)
|
|
520
|
+
copyright_holder: str | None = Field(
|
|
521
|
+
default=None,
|
|
522
|
+
description=(
|
|
523
|
+
"Copyright holder organisation (Metanorma :copyright-holder:). Also drives "
|
|
524
|
+
"the PDF footer organisation name, replacing the flavor default (e.g. 'OPEN "
|
|
525
|
+
"GEOSPATIAL CONSORTIUM')."
|
|
526
|
+
),
|
|
527
|
+
)
|
|
528
|
+
boilerplate: Boilerplate | None = Field(
|
|
529
|
+
default=None,
|
|
530
|
+
description=(
|
|
531
|
+
"Front-matter legal boilerplate (copyright/license/legal/feedback) replacing "
|
|
532
|
+
"the flavor's built-in text via :boilerplate-authority:."
|
|
533
|
+
),
|
|
534
|
+
)
|
|
535
|
+
suppress_flavor_logo: bool = Field(
|
|
536
|
+
default=False,
|
|
537
|
+
description=(
|
|
538
|
+
"When true, suppress the Metanorma flavor's built-in logo on the preface/"
|
|
539
|
+
"legal page (e.g. the OGC logo) via a targeted PDF stylesheet override. "
|
|
540
|
+
"Use when rebranding (e.g. DGIWG) so no OGC logo remains on internal pages. "
|
|
541
|
+
"Note: this overrides an OGC-flavor XSL template and is coupled to the OGC "
|
|
542
|
+
"Metanorma flavor."
|
|
543
|
+
),
|
|
544
|
+
)
|
|
388
545
|
|
|
389
546
|
@field_validator("submission_date", "approval_date", "publication_date")
|
|
390
547
|
@classmethod
|
|
@@ -664,6 +821,14 @@ class ServiceProfile(BaseModel):
|
|
|
664
821
|
if vrs:
|
|
665
822
|
_check(f"Collection '{coll.id}' extent.vertical.vrs", vrs, er.extent_vrs)
|
|
666
823
|
|
|
824
|
+
# --- extent.vertical.positive required? ---
|
|
825
|
+
if er.require_vertical_direction and coll.extent and coll.extent.vertical:
|
|
826
|
+
if getattr(coll.extent.vertical, "positive", None) not in ("up", "down"):
|
|
827
|
+
raise ValueError(
|
|
828
|
+
f"Collection '{coll.id}' extent.vertical.positive must be "
|
|
829
|
+
f"'up' or 'down' (extent_requirements.require_vertical_direction is set)"
|
|
830
|
+
)
|
|
831
|
+
|
|
667
832
|
# --- top-level crs array → supported_crs ---
|
|
668
833
|
if coll.crs and er.supported_crs:
|
|
669
834
|
for crs_val in coll.crs:
|
{oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.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.
|
|
3
|
+
Version: 3.2.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
|
|
@@ -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 |
|
|
@@ -6,6 +6,7 @@ src/oapi_profile_builder/cite.py
|
|
|
6
6
|
src/oapi_profile_builder/cite_features.py
|
|
7
7
|
src/oapi_profile_builder/cli.py
|
|
8
8
|
src/oapi_profile_builder/compile.py
|
|
9
|
+
src/oapi_profile_builder/cover.py
|
|
9
10
|
src/oapi_profile_builder/generate.py
|
|
10
11
|
src/oapi_profile_builder/models.py
|
|
11
12
|
src/oapi_profile_builder/server_validation.py
|
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/cite_features.py
RENAMED
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-3.1.0 → oapi_profile_builder-3.2.0}/src/oapi_profile_builder/compile.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|