oapi-profile-builder 3.5.0__tar.gz → 3.6.2__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.5.0/src/oapi_profile_builder.egg-info → oapi_profile_builder-3.6.2}/PKG-INFO +74 -9
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/README.md +73 -8
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/pyproject.toml +1 -1
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/cover.py +71 -27
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/generate.py +70 -49
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/models.py +33 -6
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2/src/oapi_profile_builder.egg-info}/PKG-INFO +74 -9
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/LICENSE +0 -0
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/setup.cfg +0 -0
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/__init__.py +0 -0
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/cite.py +0 -0
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/cite_features.py +0 -0
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/cli.py +0 -0
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/compile.py +0 -0
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/server_validation.py +0 -0
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder.egg-info/SOURCES.txt +0 -0
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder.egg-info/dependency_links.txt +0 -0
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder.egg-info/entry_points.txt +0 -0
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder.egg-info/requires.txt +0 -0
- {oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder.egg-info/top_level.txt +0 -0
{oapi_profile_builder-3.5.0/src/oapi_profile_builder.egg-info → oapi_profile_builder-3.6.2}/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.6.2
|
|
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
|
|
@@ -47,7 +47,7 @@ Dynamic: license-file
|
|
|
47
47
|
|
|
48
48
|
# OGC API Service Profile Builder
|
|
49
49
|
|
|
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.
|
|
50
|
+
Generate OGC API - EDR Part 3 Service Profile artifacts from a YAML config — OpenAPI 3.0.0/3.1.0, AsyncAPI, AsciiDoc requirements and conformance tests, and a publication-ready Metanorma PDF with optional custom (e.g. DGIWG) branding.
|
|
51
51
|
|
|
52
52
|
[](https://pypi.org/project/oapi-profile-builder/)
|
|
53
53
|
[](LICENSE)
|
|
@@ -57,7 +57,9 @@ Generate OGC API - EDR Part 3 Service Profile artifacts from a YAML config — O
|
|
|
57
57
|
## Quick Start
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
|
|
60
|
+
# Base install. Adds Pillow (cover image generation) with the [pdf] extra —
|
|
61
|
+
# required whenever your profile sets document_metadata.cover.logo.
|
|
62
|
+
pip install 'oapi-profile-builder[pdf]'
|
|
61
63
|
|
|
62
64
|
# Copy an example config and edit it
|
|
63
65
|
cp examples/minimal_profile.yaml my_profile.yaml
|
|
@@ -70,7 +72,7 @@ That's it. The `output/` directory will contain:
|
|
|
70
72
|
|
|
71
73
|
```
|
|
72
74
|
output/
|
|
73
|
-
├── openapi.yaml # OpenAPI 3.1.0 — ready for Swagger UI, Redoc, schemathesis
|
|
75
|
+
├── openapi.yaml # OpenAPI 3.0.0 or 3.1.0 — ready for Swagger UI, Redoc, schemathesis
|
|
74
76
|
├── profile_config.json # Round-trip serialized profile
|
|
75
77
|
├── document.adoc # Metanorma root document
|
|
76
78
|
├── sections/ # Abstract, Preface, Scope, Conformance, References, Terms
|
|
@@ -181,6 +183,30 @@ OGC API - EDR CITE Results
|
|
|
181
183
|
|
|
182
184
|
## GitHub Actions
|
|
183
185
|
|
|
186
|
+
### Self-service: generate artifacts from an issue (no fork, no install)
|
|
187
|
+
|
|
188
|
+
Anyone with a GitHub account can generate artifacts from their own config without cloning or installing anything:
|
|
189
|
+
|
|
190
|
+
1. Open a new issue using the **"Generate profile artifacts"** template ([New issue](../../issues/new/choose)).
|
|
191
|
+
2. Paste your profile YAML into the config field.
|
|
192
|
+
3. If your profile references local files (typically a cover logo via `document_metadata.cover.logo`), drag-and-drop them into the **"Additional assets (optional)"** field. Reference each uploaded file in your YAML as `uploads/<original-filename>` — for example:
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
document_metadata:
|
|
196
|
+
cover:
|
|
197
|
+
logo: uploads/my_logo.png
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The workflow downloads each dropped file into `./uploads/` before generation. It parses your YAML to figure out the target filename, so images that GitHub renders as an `<img>` tag with a UUID URL still land at the exact path your config references. Tip: to skip the upload entirely for the DGIWG logo, point at the copy that already ships in this repo: `logo: examples/assets/DGIWG_logo.png`.
|
|
201
|
+
4. Tick "Compile a PDF?" if you also want a Metanorma PDF, then submit.
|
|
202
|
+
5. A workflow validates the config, generates the artifacts, and comments on the issue with a link to download them as a zip (retained for 14 days).
|
|
203
|
+
|
|
204
|
+
The workflow only runs the generator over your declarative config — it never executes user-supplied code — and comments back on both success and failure. Don't paste secrets: the artifacts are attached to a public workflow run.
|
|
205
|
+
|
|
206
|
+
> Maintainers: create a `profile-request` label so requests are easy to filter (the workflow gates on a marker in the issue body, so it works even before the label exists).
|
|
207
|
+
|
|
208
|
+
### Use the action in your own workflow
|
|
209
|
+
|
|
184
210
|
No local install needed. Add this to any workflow to generate profile artifacts from a config file:
|
|
185
211
|
|
|
186
212
|
```yaml
|
|
@@ -239,7 +265,7 @@ Everything about the document — cover, title-page metadata, legal page, footer
|
|
|
239
265
|
| PDF colours (titles, tables, cover, backgrounds) | `document_metadata.colors` | `:presentation-metadata-color-*:` |
|
|
240
266
|
| Page-ii legal text (copyright / license / notice) | `document_metadata.boilerplate` | `:boilerplate-authority:` |
|
|
241
267
|
| Footer organisation name | `document_metadata.copyright_holder` | `:copyright-holder:` |
|
|
242
|
-
| Cover layout (logo size/position, tagline/title size, bold edition) | `document_metadata.cover.{logo_width, logo_y, tagline_font_size, title_font_size, bold_edition}` | Generated cover image |
|
|
268
|
+
| Cover layout (logo size/position, tagline/title/edition-date size, bold edition+date) | `document_metadata.cover.{logo_width, logo_y, tagline_font_size, title_font_size, edition_date_font_size, bold_edition}` | Generated cover image |
|
|
243
269
|
| Cover date | `document_metadata.doc_pub_date` | Long-format date on the generated cover + `:published-date:` |
|
|
244
270
|
| Remove the OGC logo from the legal page | `document_metadata.suppress_flavor_logo` | targeted `:pdf-stylesheet-override:` |
|
|
245
271
|
| Remove OGC design (crossing lines, circled numbers, rules, navy divider pages) | `document_metadata.suppress_design_elements` (or the individual `suppress_crossing_lines` / `plain_section_numbers` / `suppress_title_underlines`) | `:pdf-stylesheet-override:` + colour attributes |
|
|
@@ -352,6 +378,7 @@ abstract_tests: []
|
|
|
352
378
|
| `processes` | list | no | OGC API Processes to expose in the OpenAPI |
|
|
353
379
|
| `requirements` | list | no | Normative requirements for the AsciiDoc/PDF |
|
|
354
380
|
| `abstract_tests` | list | no | Conformance tests — each must reference a valid requirement `id` |
|
|
381
|
+
| `abstract_tests[].method` | string | no | Optional test method description |
|
|
355
382
|
| `pubsub` | object | no | OGC API - EDR Part 2 PubSub config — generates `asyncapi.yaml` |
|
|
356
383
|
| `collection_examples` | object | no | `{collectionId: {instanceId: "..."}}` — used by `validate-server` |
|
|
357
384
|
| `paging` | object | no | Features `/items` paging — adds a validated `limit` query parameter to the generated OpenAPI (see below) |
|
|
@@ -646,6 +673,7 @@ requirements:
|
|
|
646
673
|
abstract_tests:
|
|
647
674
|
- id: position-coveragejson # must equal requirement_id
|
|
648
675
|
requirement_id: position-coveragejson
|
|
676
|
+
method: Manual inspection of the /position response.
|
|
649
677
|
steps:
|
|
650
678
|
- Send GET /collections/{id}/position?coords=POINT(lon lat).
|
|
651
679
|
- Verify Content-Type is application/prs.coverage+json.
|
|
@@ -695,7 +723,7 @@ When present, generates `asyncapi.yaml` alongside `openapi.yaml`.
|
|
|
695
723
|
pubsub:
|
|
696
724
|
broker_host: my-broker.example.com
|
|
697
725
|
broker_port: 5672
|
|
698
|
-
protocol: amqp # amqp | mqtt | kafka
|
|
726
|
+
protocol: amqp # amqp | amqp1 | mqtt | kafka
|
|
699
727
|
collections:
|
|
700
728
|
- my_collection
|
|
701
729
|
filters:
|
|
@@ -704,6 +732,40 @@ pubsub:
|
|
|
704
732
|
type: string
|
|
705
733
|
```
|
|
706
734
|
|
|
735
|
+
`broker_host`/`broker_port`/`protocol` synthesise a single implicit
|
|
736
|
+
`production` AsyncAPI server. To describe an exact transport topology instead —
|
|
737
|
+
e.g. a native WebSocket `/ws` endpoint plus AMQP 1.0 and MQTT brokers, as the
|
|
738
|
+
`nws_connect_profile.yaml` example does — list them explicitly under `servers`.
|
|
739
|
+
When `servers` is set, the implicit `production` server is **not** emitted:
|
|
740
|
+
|
|
741
|
+
```yaml
|
|
742
|
+
pubsub:
|
|
743
|
+
broker_host: edr-broker
|
|
744
|
+
broker_port: 5672
|
|
745
|
+
protocol: amqp1 # AMQP 1.0 (e.g. RabbitMQ 4.x native)
|
|
746
|
+
collections: [water_gauge, wwa]
|
|
747
|
+
servers:
|
|
748
|
+
- name: websocket_secure
|
|
749
|
+
description: Native WebSocket live map (production)
|
|
750
|
+
host: ''
|
|
751
|
+
protocol: wss # amqp | amqp1 | mqtt | kafka | ws | wss
|
|
752
|
+
pathname: /ws
|
|
753
|
+
- name: amqp
|
|
754
|
+
description: AMQP 1.0 broker (RabbitMQ 4.x native AMQP 1.0)
|
|
755
|
+
host: 'edr-broker:5672'
|
|
756
|
+
protocol: amqp1
|
|
757
|
+
- name: mqtt
|
|
758
|
+
description: MQTT broker (RabbitMQ MQTT plugin)
|
|
759
|
+
host: 'edr-broker:1883'
|
|
760
|
+
protocol: mqtt
|
|
761
|
+
collection_filters: # per-collection filter overrides
|
|
762
|
+
water_gauge:
|
|
763
|
+
filters:
|
|
764
|
+
- name: flood_stages
|
|
765
|
+
description: Alert for selected flood stages
|
|
766
|
+
type: array
|
|
767
|
+
```
|
|
768
|
+
|
|
707
769
|
---
|
|
708
770
|
|
|
709
771
|
### `document_metadata`
|
|
@@ -764,7 +826,9 @@ Two `document_metadata` blocks let you rebrand the PDF (e.g. for DGIWG) without
|
|
|
764
826
|
document_metadata:
|
|
765
827
|
doc_number: "DGIWG 134"
|
|
766
828
|
cover:
|
|
767
|
-
logo: assets/
|
|
829
|
+
logo: examples/assets/DGIWG_logo.png # path relative to the working directory (or absolute).
|
|
830
|
+
# From the self-service issue form use uploads/<file>
|
|
831
|
+
# instead and drag-drop the file into "Additional assets".
|
|
768
832
|
tagline: Delivering Military Advantage through multi-national geospatial interoperability
|
|
769
833
|
background: "#FFFFFF" # optional cover background (default white)
|
|
770
834
|
text_color: "#1F3864" # optional cover text colour (default black)
|
|
@@ -773,11 +837,12 @@ document_metadata:
|
|
|
773
837
|
font_italic: "Source Sans Pro" # optional italic face for the tagline
|
|
774
838
|
watermark: "DRAFT" # optional diagonal watermark stamped on the cover
|
|
775
839
|
# --- cover layout (all optional) ---
|
|
776
|
-
logo_width:
|
|
840
|
+
logo_width: 300 # target logo width in px (default 420)
|
|
777
841
|
logo_y: 100 # logo vertical offset from top in px (default 180)
|
|
778
842
|
tagline_font_size: 20 # tagline point size (default 30)
|
|
779
843
|
title_font_size: 40 # title point size (default 54)
|
|
780
|
-
|
|
844
|
+
edition_date_font_size: 22 # edition/date point size (default 24)
|
|
845
|
+
bold_edition: true # render the "Edition X" and "Dated ..." lines in bold (default false)
|
|
781
846
|
colors:
|
|
782
847
|
text: "#000000" # body text
|
|
783
848
|
cover_text: "#1F3864" # cover text / section numbers / ToC
|
|
@@ -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 and conformance tests, and a publication-ready Metanorma PDF with optional custom (e.g. DGIWG) branding.
|
|
3
|
+
Generate OGC API - EDR Part 3 Service Profile artifacts from a YAML config — OpenAPI 3.0.0/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)
|
|
@@ -10,7 +10,9 @@ Generate OGC API - EDR Part 3 Service Profile artifacts from a YAML config — O
|
|
|
10
10
|
## Quick Start
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
|
|
13
|
+
# Base install. Adds Pillow (cover image generation) with the [pdf] extra —
|
|
14
|
+
# required whenever your profile sets document_metadata.cover.logo.
|
|
15
|
+
pip install 'oapi-profile-builder[pdf]'
|
|
14
16
|
|
|
15
17
|
# Copy an example config and edit it
|
|
16
18
|
cp examples/minimal_profile.yaml my_profile.yaml
|
|
@@ -23,7 +25,7 @@ That's it. The `output/` directory will contain:
|
|
|
23
25
|
|
|
24
26
|
```
|
|
25
27
|
output/
|
|
26
|
-
├── openapi.yaml # OpenAPI 3.1.0 — ready for Swagger UI, Redoc, schemathesis
|
|
28
|
+
├── openapi.yaml # OpenAPI 3.0.0 or 3.1.0 — ready for Swagger UI, Redoc, schemathesis
|
|
27
29
|
├── profile_config.json # Round-trip serialized profile
|
|
28
30
|
├── document.adoc # Metanorma root document
|
|
29
31
|
├── sections/ # Abstract, Preface, Scope, Conformance, References, Terms
|
|
@@ -134,6 +136,30 @@ OGC API - EDR CITE Results
|
|
|
134
136
|
|
|
135
137
|
## GitHub Actions
|
|
136
138
|
|
|
139
|
+
### Self-service: generate artifacts from an issue (no fork, no install)
|
|
140
|
+
|
|
141
|
+
Anyone with a GitHub account can generate artifacts from their own config without cloning or installing anything:
|
|
142
|
+
|
|
143
|
+
1. Open a new issue using the **"Generate profile artifacts"** template ([New issue](../../issues/new/choose)).
|
|
144
|
+
2. Paste your profile YAML into the config field.
|
|
145
|
+
3. If your profile references local files (typically a cover logo via `document_metadata.cover.logo`), drag-and-drop them into the **"Additional assets (optional)"** field. Reference each uploaded file in your YAML as `uploads/<original-filename>` — for example:
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
document_metadata:
|
|
149
|
+
cover:
|
|
150
|
+
logo: uploads/my_logo.png
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The workflow downloads each dropped file into `./uploads/` before generation. It parses your YAML to figure out the target filename, so images that GitHub renders as an `<img>` tag with a UUID URL still land at the exact path your config references. Tip: to skip the upload entirely for the DGIWG logo, point at the copy that already ships in this repo: `logo: examples/assets/DGIWG_logo.png`.
|
|
154
|
+
4. Tick "Compile a PDF?" if you also want a Metanorma PDF, then submit.
|
|
155
|
+
5. A workflow validates the config, generates the artifacts, and comments on the issue with a link to download them as a zip (retained for 14 days).
|
|
156
|
+
|
|
157
|
+
The workflow only runs the generator over your declarative config — it never executes user-supplied code — and comments back on both success and failure. Don't paste secrets: the artifacts are attached to a public workflow run.
|
|
158
|
+
|
|
159
|
+
> Maintainers: create a `profile-request` label so requests are easy to filter (the workflow gates on a marker in the issue body, so it works even before the label exists).
|
|
160
|
+
|
|
161
|
+
### Use the action in your own workflow
|
|
162
|
+
|
|
137
163
|
No local install needed. Add this to any workflow to generate profile artifacts from a config file:
|
|
138
164
|
|
|
139
165
|
```yaml
|
|
@@ -192,7 +218,7 @@ Everything about the document — cover, title-page metadata, legal page, footer
|
|
|
192
218
|
| PDF colours (titles, tables, cover, backgrounds) | `document_metadata.colors` | `:presentation-metadata-color-*:` |
|
|
193
219
|
| Page-ii legal text (copyright / license / notice) | `document_metadata.boilerplate` | `:boilerplate-authority:` |
|
|
194
220
|
| Footer organisation name | `document_metadata.copyright_holder` | `:copyright-holder:` |
|
|
195
|
-
| Cover layout (logo size/position, tagline/title size, bold edition) | `document_metadata.cover.{logo_width, logo_y, tagline_font_size, title_font_size, bold_edition}` | Generated cover image |
|
|
221
|
+
| Cover layout (logo size/position, tagline/title/edition-date size, bold edition+date) | `document_metadata.cover.{logo_width, logo_y, tagline_font_size, title_font_size, edition_date_font_size, bold_edition}` | Generated cover image |
|
|
196
222
|
| Cover date | `document_metadata.doc_pub_date` | Long-format date on the generated cover + `:published-date:` |
|
|
197
223
|
| Remove the OGC logo from the legal page | `document_metadata.suppress_flavor_logo` | targeted `:pdf-stylesheet-override:` |
|
|
198
224
|
| Remove OGC design (crossing lines, circled numbers, rules, navy divider pages) | `document_metadata.suppress_design_elements` (or the individual `suppress_crossing_lines` / `plain_section_numbers` / `suppress_title_underlines`) | `:pdf-stylesheet-override:` + colour attributes |
|
|
@@ -305,6 +331,7 @@ abstract_tests: []
|
|
|
305
331
|
| `processes` | list | no | OGC API Processes to expose in the OpenAPI |
|
|
306
332
|
| `requirements` | list | no | Normative requirements for the AsciiDoc/PDF |
|
|
307
333
|
| `abstract_tests` | list | no | Conformance tests — each must reference a valid requirement `id` |
|
|
334
|
+
| `abstract_tests[].method` | string | no | Optional test method description |
|
|
308
335
|
| `pubsub` | object | no | OGC API - EDR Part 2 PubSub config — generates `asyncapi.yaml` |
|
|
309
336
|
| `collection_examples` | object | no | `{collectionId: {instanceId: "..."}}` — used by `validate-server` |
|
|
310
337
|
| `paging` | object | no | Features `/items` paging — adds a validated `limit` query parameter to the generated OpenAPI (see below) |
|
|
@@ -599,6 +626,7 @@ requirements:
|
|
|
599
626
|
abstract_tests:
|
|
600
627
|
- id: position-coveragejson # must equal requirement_id
|
|
601
628
|
requirement_id: position-coveragejson
|
|
629
|
+
method: Manual inspection of the /position response.
|
|
602
630
|
steps:
|
|
603
631
|
- Send GET /collections/{id}/position?coords=POINT(lon lat).
|
|
604
632
|
- Verify Content-Type is application/prs.coverage+json.
|
|
@@ -648,7 +676,7 @@ When present, generates `asyncapi.yaml` alongside `openapi.yaml`.
|
|
|
648
676
|
pubsub:
|
|
649
677
|
broker_host: my-broker.example.com
|
|
650
678
|
broker_port: 5672
|
|
651
|
-
protocol: amqp # amqp | mqtt | kafka
|
|
679
|
+
protocol: amqp # amqp | amqp1 | mqtt | kafka
|
|
652
680
|
collections:
|
|
653
681
|
- my_collection
|
|
654
682
|
filters:
|
|
@@ -657,6 +685,40 @@ pubsub:
|
|
|
657
685
|
type: string
|
|
658
686
|
```
|
|
659
687
|
|
|
688
|
+
`broker_host`/`broker_port`/`protocol` synthesise a single implicit
|
|
689
|
+
`production` AsyncAPI server. To describe an exact transport topology instead —
|
|
690
|
+
e.g. a native WebSocket `/ws` endpoint plus AMQP 1.0 and MQTT brokers, as the
|
|
691
|
+
`nws_connect_profile.yaml` example does — list them explicitly under `servers`.
|
|
692
|
+
When `servers` is set, the implicit `production` server is **not** emitted:
|
|
693
|
+
|
|
694
|
+
```yaml
|
|
695
|
+
pubsub:
|
|
696
|
+
broker_host: edr-broker
|
|
697
|
+
broker_port: 5672
|
|
698
|
+
protocol: amqp1 # AMQP 1.0 (e.g. RabbitMQ 4.x native)
|
|
699
|
+
collections: [water_gauge, wwa]
|
|
700
|
+
servers:
|
|
701
|
+
- name: websocket_secure
|
|
702
|
+
description: Native WebSocket live map (production)
|
|
703
|
+
host: ''
|
|
704
|
+
protocol: wss # amqp | amqp1 | mqtt | kafka | ws | wss
|
|
705
|
+
pathname: /ws
|
|
706
|
+
- name: amqp
|
|
707
|
+
description: AMQP 1.0 broker (RabbitMQ 4.x native AMQP 1.0)
|
|
708
|
+
host: 'edr-broker:5672'
|
|
709
|
+
protocol: amqp1
|
|
710
|
+
- name: mqtt
|
|
711
|
+
description: MQTT broker (RabbitMQ MQTT plugin)
|
|
712
|
+
host: 'edr-broker:1883'
|
|
713
|
+
protocol: mqtt
|
|
714
|
+
collection_filters: # per-collection filter overrides
|
|
715
|
+
water_gauge:
|
|
716
|
+
filters:
|
|
717
|
+
- name: flood_stages
|
|
718
|
+
description: Alert for selected flood stages
|
|
719
|
+
type: array
|
|
720
|
+
```
|
|
721
|
+
|
|
660
722
|
---
|
|
661
723
|
|
|
662
724
|
### `document_metadata`
|
|
@@ -717,7 +779,9 @@ Two `document_metadata` blocks let you rebrand the PDF (e.g. for DGIWG) without
|
|
|
717
779
|
document_metadata:
|
|
718
780
|
doc_number: "DGIWG 134"
|
|
719
781
|
cover:
|
|
720
|
-
logo: assets/
|
|
782
|
+
logo: examples/assets/DGIWG_logo.png # path relative to the working directory (or absolute).
|
|
783
|
+
# From the self-service issue form use uploads/<file>
|
|
784
|
+
# instead and drag-drop the file into "Additional assets".
|
|
721
785
|
tagline: Delivering Military Advantage through multi-national geospatial interoperability
|
|
722
786
|
background: "#FFFFFF" # optional cover background (default white)
|
|
723
787
|
text_color: "#1F3864" # optional cover text colour (default black)
|
|
@@ -726,11 +790,12 @@ document_metadata:
|
|
|
726
790
|
font_italic: "Source Sans Pro" # optional italic face for the tagline
|
|
727
791
|
watermark: "DRAFT" # optional diagonal watermark stamped on the cover
|
|
728
792
|
# --- cover layout (all optional) ---
|
|
729
|
-
logo_width:
|
|
793
|
+
logo_width: 300 # target logo width in px (default 420)
|
|
730
794
|
logo_y: 100 # logo vertical offset from top in px (default 180)
|
|
731
795
|
tagline_font_size: 20 # tagline point size (default 30)
|
|
732
796
|
title_font_size: 40 # title point size (default 54)
|
|
733
|
-
|
|
797
|
+
edition_date_font_size: 22 # edition/date point size (default 24)
|
|
798
|
+
bold_edition: true # render the "Edition X" and "Dated ..." lines in bold (default false)
|
|
734
799
|
colors:
|
|
735
800
|
text: "#000000" # body text
|
|
736
801
|
cover_text: "#1F3864" # cover text / section numbers / ToC
|
|
@@ -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.6.2"
|
|
8
8
|
description = "Authoritative tooling for creating OGC API Service Profiles (EDR, Features)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -43,9 +43,9 @@ _PAGE_H = 1754
|
|
|
43
43
|
_COVER_FILENAME = "cover.png"
|
|
44
44
|
|
|
45
45
|
# Default font family (bundled with Pillow, resolved by name).
|
|
46
|
-
_DEFAULT_REGULAR = "DejaVuSans
|
|
47
|
-
_DEFAULT_BOLD = "DejaVuSans-Bold
|
|
48
|
-
_DEFAULT_ITALIC = "DejaVuSans
|
|
46
|
+
_DEFAULT_REGULAR = "DejaVuSans"
|
|
47
|
+
_DEFAULT_BOLD = "DejaVuSans-Bold"
|
|
48
|
+
_DEFAULT_ITALIC = "DejaVuSans:style=Oblique"
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
def _resolve_font_file(spec: str | None, *, bold: bool = False, italic: bool = False) -> str | None:
|
|
@@ -62,20 +62,40 @@ def _resolve_font_file(spec: str | None, *, bold: bool = False, italic: bool = F
|
|
|
62
62
|
if p.is_file():
|
|
63
63
|
return str(p)
|
|
64
64
|
if shutil.which("fc-match"):
|
|
65
|
-
|
|
66
|
-
if
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
styles
|
|
70
|
-
|
|
65
|
+
# If the spec already contains a colon, treat it as a full fontconfig query
|
|
66
|
+
if ":" in spec:
|
|
67
|
+
query = spec
|
|
68
|
+
else:
|
|
69
|
+
styles = []
|
|
70
|
+
if bold:
|
|
71
|
+
styles.append("Bold")
|
|
72
|
+
if italic:
|
|
73
|
+
styles.append("Italic")
|
|
74
|
+
query = spec if not styles else f"{spec}:style={' '.join(styles)}"
|
|
75
|
+
|
|
71
76
|
try:
|
|
77
|
+
# Try the primary query
|
|
72
78
|
out = subprocess.run(
|
|
73
79
|
["fc-match", "-f", "%{file}", query],
|
|
74
80
|
capture_output=True, text=True, timeout=5, check=False,
|
|
75
81
|
)
|
|
76
82
|
path = out.stdout.strip()
|
|
77
83
|
if path and Path(path).is_file():
|
|
78
|
-
|
|
84
|
+
# Verify that the result matches the requested style if possible.
|
|
85
|
+
# If we asked for Italic but got a Regular file, fc-match just fell back.
|
|
86
|
+
if italic and "Italic" not in path and "Oblique" not in path:
|
|
87
|
+
# Try a second attempt explicitly asking for Oblique if Italic failed
|
|
88
|
+
if ":" not in spec:
|
|
89
|
+
query_oblique = f"{spec}:style=Oblique"
|
|
90
|
+
out = subprocess.run(
|
|
91
|
+
["fc-match", "-f", "%{file}", query_oblique],
|
|
92
|
+
capture_output=True, text=True, timeout=5, check=False,
|
|
93
|
+
)
|
|
94
|
+
path = out.stdout.strip()
|
|
95
|
+
if path and Path(path).is_file() and ("Italic" in path or "Oblique" in path):
|
|
96
|
+
return path
|
|
97
|
+
else:
|
|
98
|
+
return path
|
|
79
99
|
except Exception:
|
|
80
100
|
pass
|
|
81
101
|
return None
|
|
@@ -89,22 +109,36 @@ class _FontSet:
|
|
|
89
109
|
ultimately fall back to the bundled DejaVu family.
|
|
90
110
|
"""
|
|
91
111
|
|
|
92
|
-
def __init__(self, cover=None):
|
|
112
|
+
def __init__(self, cover=None, metadata=None):
|
|
93
113
|
self.regular = getattr(cover, "font_regular", None) if cover else None
|
|
94
114
|
self.bold = getattr(cover, "font_bold", None) if cover else None
|
|
95
115
|
self.italic = getattr(cover, "font_italic", None) if cover else None
|
|
116
|
+
|
|
117
|
+
# Fallback to metadata-level font overrides if set
|
|
118
|
+
if not self.regular and metadata:
|
|
119
|
+
self.regular = getattr(metadata, "body_font", None)
|
|
120
|
+
if not self.bold and metadata:
|
|
121
|
+
self.bold = getattr(metadata, "header_font", None)
|
|
96
122
|
|
|
97
123
|
def _font(self, size: int, *, bold: bool = False, italic: bool = False):
|
|
98
124
|
from PIL import ImageFont
|
|
99
125
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
_resolve_font_file(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
126
|
+
candidates = []
|
|
127
|
+
# 1. Try explicit style override (path or family name)
|
|
128
|
+
style_spec = self.bold if bold else (self.italic if italic else None)
|
|
129
|
+
if style_spec:
|
|
130
|
+
candidates.append(_resolve_font_file(style_spec, bold=bold, italic=italic))
|
|
131
|
+
|
|
132
|
+
# 2. Try deriving from the regular spec if it is a family name
|
|
133
|
+
if (bold or italic) and self.regular and not Path(self.regular).expanduser().is_file():
|
|
134
|
+
candidates.append(_resolve_font_file(self.regular, bold=bold, italic=italic))
|
|
135
|
+
|
|
136
|
+
# 3. Try standard defaults for the requested style
|
|
137
|
+
candidates.append(_DEFAULT_BOLD if bold else (_DEFAULT_ITALIC if italic else _DEFAULT_REGULAR))
|
|
138
|
+
|
|
139
|
+
# 4. Fallback to the regular font spec (even if not the right style)
|
|
140
|
+
candidates.append(_resolve_font_file(self.regular))
|
|
141
|
+
|
|
108
142
|
for cand in candidates:
|
|
109
143
|
if not cand:
|
|
110
144
|
continue
|
|
@@ -160,7 +194,7 @@ def build_cover_image(profile, output_dir: Path) -> str | None:
|
|
|
160
194
|
|
|
161
195
|
bg = cover.background or "#FFFFFF"
|
|
162
196
|
fg = cover.text_color or "#000000"
|
|
163
|
-
fonts = _FontSet(cover)
|
|
197
|
+
fonts = _FontSet(cover, m)
|
|
164
198
|
|
|
165
199
|
page = Image.new("RGB", (_PAGE_W, _PAGE_H), bg)
|
|
166
200
|
draw = ImageDraw.Draw(page)
|
|
@@ -191,23 +225,33 @@ def build_cover_image(profile, output_dir: Path) -> str | None:
|
|
|
191
225
|
# --- Tagline (italic) ---
|
|
192
226
|
if cover.tagline:
|
|
193
227
|
tagline_fs = getattr(cover, "tagline_font_size", 30)
|
|
194
|
-
|
|
228
|
+
tagline_font = fonts.italic_font(tagline_fs)
|
|
229
|
+
for line in _wrap(draw, cover.tagline, tagline_font, _PAGE_W - 240):
|
|
230
|
+
y = centered(line, tagline_font, y, fg) + 10
|
|
231
|
+
y += 50
|
|
195
232
|
|
|
196
233
|
# --- Document number ---
|
|
197
234
|
if m and m.doc_number:
|
|
198
|
-
|
|
235
|
+
doc_num_font = fonts.regular_font(46, bold=True)
|
|
236
|
+
for line in _wrap(draw, m.doc_number, doc_num_font, _PAGE_W - 240):
|
|
237
|
+
y = centered(line, doc_num_font, y, fg) + 16
|
|
238
|
+
y += 24
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# --- Edition & date ---
|
|
242
|
+
# Per DGIWG feedback these two lines are both bold and set a little smaller
|
|
243
|
+
# than the document number so they read as a compact sub-heading.
|
|
244
|
+
ed_date_fs = getattr(cover, "edition_date_font_size", 24)
|
|
245
|
+
bold_ed = bool(getattr(cover, "bold_edition", False))
|
|
199
246
|
|
|
200
|
-
# --- Edition ---
|
|
201
247
|
edition = getattr(profile, "version", None)
|
|
202
248
|
if edition:
|
|
203
|
-
|
|
204
|
-
y = centered(f"Edition {edition}", fonts.regular_font(28, bold=bold_ed), y, fg) + 24
|
|
249
|
+
y = centered(f"Edition {edition}", fonts.regular_font(ed_date_fs, bold=bold_ed), y, fg) + 20
|
|
205
250
|
|
|
206
|
-
# --- Date ---
|
|
207
251
|
pub_date = (m.doc_pub_date or m.publication_date) if m else None
|
|
208
252
|
date_str = _format_date(pub_date)
|
|
209
253
|
if date_str:
|
|
210
|
-
y = centered(f"Dated {date_str}", fonts.regular_font(
|
|
254
|
+
y = centered(f"Dated {date_str}", fonts.regular_font(ed_date_fs, bold=bold_ed), y, fg) + 80
|
|
211
255
|
|
|
212
256
|
# --- Title (large, bold, wrapped) ---
|
|
213
257
|
title = getattr(profile, "title", "") or ""
|
{oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/generate.py
RENAMED
|
@@ -41,6 +41,16 @@ import yaml
|
|
|
41
41
|
|
|
42
42
|
from edr_pydantic.collections import Collection
|
|
43
43
|
from oapi_profile_builder.models import ServiceProfile
|
|
44
|
+
def _const(val, version: str) -> dict:
|
|
45
|
+
if version == "3.1.0":
|
|
46
|
+
return {"const": val}
|
|
47
|
+
return {"enum": [val]}
|
|
48
|
+
|
|
49
|
+
def _contains(schema: dict, version: str) -> dict:
|
|
50
|
+
if version == "3.1.0":
|
|
51
|
+
return {"contains": schema}
|
|
52
|
+
return {}
|
|
53
|
+
|
|
44
54
|
|
|
45
55
|
|
|
46
56
|
class _NoAliasDumper(yaml.Dumper):
|
|
@@ -125,30 +135,30 @@ _LINK_SCHEMA = {
|
|
|
125
135
|
_LINKS_ARRAY = {"type": "array", "items": _LINK_SCHEMA}
|
|
126
136
|
|
|
127
137
|
# Landing page response with required profile link per REQ_publishing
|
|
128
|
-
def _landing_page_schema(profile_uri: str, title: str = "", description: str = "", keywords: list | None = None) -> dict:
|
|
138
|
+
def _landing_page_schema(profile_uri: str, version: str, title: str = "", description: str = "", keywords: list | None = None) -> dict:
|
|
129
139
|
schema_props: dict = {
|
|
130
140
|
"links": {
|
|
131
141
|
"type": "array",
|
|
132
142
|
"items": _LINK_SCHEMA,
|
|
133
|
-
|
|
143
|
+
**(_contains({
|
|
134
144
|
"type": "object",
|
|
135
145
|
"required": ["href", "rel"],
|
|
136
146
|
"properties": {
|
|
137
|
-
"rel":
|
|
138
|
-
"href":
|
|
147
|
+
"rel": _const("profile", version),
|
|
148
|
+
"href": _const(profile_uri, version),
|
|
139
149
|
},
|
|
140
|
-
},
|
|
150
|
+
}, version) if version == "3.1.0" else {}),
|
|
141
151
|
},
|
|
142
152
|
}
|
|
143
153
|
if title:
|
|
144
|
-
schema_props["title"] = {"type": "string",
|
|
154
|
+
schema_props["title"] = {"type": "string", **_const(title, version)}
|
|
145
155
|
if description:
|
|
146
156
|
schema_props["description"] = {"type": "string"}
|
|
147
157
|
if keywords:
|
|
148
158
|
schema_props["keywords"] = {
|
|
149
159
|
"type": "array",
|
|
150
160
|
"items": {"type": "string"},
|
|
151
|
-
"
|
|
161
|
+
**(_contains({"enum": keywords}, version) if version == "3.1.0" else {"items": {"type": "string", "enum": keywords}}),
|
|
152
162
|
}
|
|
153
163
|
|
|
154
164
|
return {
|
|
@@ -182,27 +192,30 @@ _R200_CONFORMANCE = {
|
|
|
182
192
|
}
|
|
183
193
|
}
|
|
184
194
|
_R200_COLLECTION = {"description": "Collection metadata", "content": {"application/json": {"schema": {"type": "object", "required": ["id"], "properties": {"id": {"type": "string"}, "title": {"type": "string"}, "description": {"type": "string"}, "links": _LINKS_ARRAY}}}}}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
|
|
192
|
-
"
|
|
193
|
-
|
|
194
|
-
"
|
|
195
|
-
|
|
195
|
+
def _features_schema(version: str) -> dict:
|
|
196
|
+
return {
|
|
197
|
+
"description": "Feature collection",
|
|
198
|
+
"content": {
|
|
199
|
+
"application/json": {
|
|
200
|
+
"schema": {
|
|
201
|
+
"type": "object",
|
|
202
|
+
"properties": {
|
|
203
|
+
"type": {"type": "string", **_const("FeatureCollection", version)},
|
|
204
|
+
"features": {
|
|
205
|
+
"type": "array",
|
|
206
|
+
"items": {"type": "object"},
|
|
207
|
+
},
|
|
196
208
|
},
|
|
197
209
|
},
|
|
198
210
|
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
211
|
+
},
|
|
212
|
+
}
|
|
202
213
|
|
|
203
214
|
def _locations_response_schema(profile: "ServiceProfile | None") -> dict:
|
|
204
215
|
"""Build the 200 response for a /locations query.
|
|
205
216
|
|
|
217
|
+
version = profile.openapi_version if profile else "3.0.0"
|
|
218
|
+
|
|
206
219
|
Per the EDR locations-response requirement (and the MetOcean profile's
|
|
207
220
|
/req/core/locations_query_response_format), the response is a GeoJSON
|
|
208
221
|
FeatureCollection where every feature carries a string ``id`` and a
|
|
@@ -223,7 +236,7 @@ def _locations_response_schema(profile: "ServiceProfile | None") -> dict:
|
|
|
223
236
|
"type": "object",
|
|
224
237
|
"required": ["type", "id", "properties"],
|
|
225
238
|
"properties": {
|
|
226
|
-
"type": {"type": "string", "
|
|
239
|
+
"type": {"type": "string", **_const("Feature", version)},
|
|
227
240
|
"id": {"type": "string", "description": "Location identifier usable at /locations/{locId}"},
|
|
228
241
|
"geometry": {"type": "object", "description": "GeoJSON geometry of the location"},
|
|
229
242
|
"properties": properties_schema,
|
|
@@ -237,7 +250,7 @@ def _locations_response_schema(profile: "ServiceProfile | None") -> dict:
|
|
|
237
250
|
"type": "object",
|
|
238
251
|
"required": ["type", "features"],
|
|
239
252
|
"properties": {
|
|
240
|
-
"type": {"type": "string", "
|
|
253
|
+
"type": {"type": "string", **_const("FeatureCollection", version)},
|
|
241
254
|
"features": {"type": "array", "items": feature_schema},
|
|
242
255
|
},
|
|
243
256
|
}
|
|
@@ -267,11 +280,11 @@ def _coverage_response(coll: Collection, profile: "ServiceProfile | None") -> di
|
|
|
267
280
|
media_type = "application/prs.coverage+json"
|
|
268
281
|
# Also accept the newer media type
|
|
269
282
|
alt_media = "application/vnd.cov+json"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
283
|
+
if schema_ref:
|
|
284
|
+
schema = {"$ref": schema_ref}
|
|
285
|
+
else:
|
|
286
|
+
# Fallback to an inline object if external ref is problematic
|
|
287
|
+
schema = {"type": "object"}
|
|
275
288
|
content[media_type] = {"schema": schema}
|
|
276
289
|
content[alt_media] = {"schema": schema}
|
|
277
290
|
elif fmt_name == "GeoJSON":
|
|
@@ -517,6 +530,7 @@ def _collection_response_schema(coll: Collection,
|
|
|
517
530
|
- Full parameter_names objects with all fields
|
|
518
531
|
- parameter_name_pattern as propertyNames constraint
|
|
519
532
|
"""
|
|
533
|
+
version = profile.openapi_version if profile else "3.0.0"
|
|
520
534
|
crs_schema: dict = {"type": "string"} # extent.spatial.crs
|
|
521
535
|
trs_schema: dict = {"type": "string"} # extent.temporal.trs
|
|
522
536
|
vrs_schema: dict = {"type": "string"} # extent.vertical.vrs
|
|
@@ -549,13 +563,17 @@ def _collection_response_schema(coll: Collection,
|
|
|
549
563
|
import copy
|
|
550
564
|
param_item_schema = copy.deepcopy(profile.parameter_schema)
|
|
551
565
|
else:
|
|
552
|
-
param_item_schema = _parameter_schema()
|
|
566
|
+
param_item_schema = _parameter_schema(profile.openapi_version)
|
|
553
567
|
|
|
554
568
|
param_names_schema: dict = {
|
|
555
569
|
"type": "object",
|
|
556
570
|
"additionalProperties": param_item_schema,
|
|
557
571
|
}
|
|
558
|
-
if
|
|
572
|
+
if coll.parameter_names:
|
|
573
|
+
param_names_schema["properties"] = {
|
|
574
|
+
name: param_item_schema for name in coll.parameter_names.root
|
|
575
|
+
}
|
|
576
|
+
if version == "3.1.0" and profile and profile.parameter_name_pattern:
|
|
559
577
|
param_names_schema["propertyNames"] = {
|
|
560
578
|
"type": "string",
|
|
561
579
|
"pattern": profile.parameter_name_pattern,
|
|
@@ -614,7 +632,7 @@ def _collection_response_schema(coll: Collection,
|
|
|
614
632
|
"type": "array",
|
|
615
633
|
"items": {
|
|
616
634
|
"type": "array",
|
|
617
|
-
"items": {
|
|
635
|
+
"items": {"type": "string", "format": "date-time", "nullable": True} if version == "3.0.0" else {
|
|
618
636
|
"oneOf": [
|
|
619
637
|
{"type": "string", "format": "date-time"},
|
|
620
638
|
{"type": "null"},
|
|
@@ -734,13 +752,13 @@ def _collection_response_schema(coll: Collection,
|
|
|
734
752
|
}
|
|
735
753
|
|
|
736
754
|
|
|
737
|
-
def _parameter_schema() -> dict:
|
|
755
|
+
def _parameter_schema(version: str) -> dict:
|
|
738
756
|
"""OpenAPI schema for a single EDR Parameter object."""
|
|
739
757
|
return {
|
|
740
758
|
"type": "object",
|
|
741
759
|
"required": ["type", "observedProperty"],
|
|
742
760
|
"properties": {
|
|
743
|
-
"type": {"type": "string", "
|
|
761
|
+
"type": {"type": "string", **_const("Parameter", version)},
|
|
744
762
|
"id": {"type": "string"},
|
|
745
763
|
"label": {"type": "string"},
|
|
746
764
|
"description": {"type": "string"},
|
|
@@ -930,7 +948,7 @@ def _collection_paths(coll: Collection, examples: dict | None = None,
|
|
|
930
948
|
"tags": [tag],
|
|
931
949
|
"parameters": items_params,
|
|
932
950
|
"responses": {
|
|
933
|
-
"200":
|
|
951
|
+
"200": _features_schema(profile.openapi_version if profile else "3.0.0"),
|
|
934
952
|
"400": _ERR_400, "404": _ERR_404, "500": _ERR_500,
|
|
935
953
|
},
|
|
936
954
|
}}
|
|
@@ -944,7 +962,7 @@ def _collection_paths(coll: Collection, examples: dict | None = None,
|
|
|
944
962
|
_F, _LANG,
|
|
945
963
|
],
|
|
946
964
|
"responses": {
|
|
947
|
-
"200":
|
|
965
|
+
"200": _features_schema(profile.openapi_version if profile else "3.0.0"),
|
|
948
966
|
"400": _ERR_400, "404": _ERR_404, "500": _ERR_500,
|
|
949
967
|
},
|
|
950
968
|
}}
|
|
@@ -970,7 +988,7 @@ def _collection_paths(coll: Collection, examples: dict | None = None,
|
|
|
970
988
|
"tags": [tag],
|
|
971
989
|
"parameters": [_F],
|
|
972
990
|
"responses": {
|
|
973
|
-
"200":
|
|
991
|
+
"200": _features_schema(profile.openapi_version if profile else "3.0.0"),
|
|
974
992
|
"400": _ERR_400, "500": _ERR_500,
|
|
975
993
|
},
|
|
976
994
|
}}
|
|
@@ -988,7 +1006,7 @@ def _collection_paths(coll: Collection, examples: dict | None = None,
|
|
|
988
1006
|
"operationId": _operation_id("getInstance", coll.id),
|
|
989
1007
|
"tags": [tag],
|
|
990
1008
|
"parameters": [instance_id_param, _F],
|
|
991
|
-
"responses": {"200":
|
|
1009
|
+
"responses": {"200": _features_schema(profile.openapi_version if profile else "3.0.0")},
|
|
992
1010
|
}}
|
|
993
1011
|
# instance-level query sub-paths — GET + optional POST
|
|
994
1012
|
for sub_qt in (active - {"instances"}):
|
|
@@ -1077,7 +1095,7 @@ def _collection_paths(coll: Collection, examples: dict | None = None,
|
|
|
1077
1095
|
|
|
1078
1096
|
def _core_paths(profile: ServiceProfile) -> dict:
|
|
1079
1097
|
landing_response = _landing_page_schema(
|
|
1080
|
-
profile.req_uri,
|
|
1098
|
+
profile.req_uri, profile.openapi_version,
|
|
1081
1099
|
title=profile.title,
|
|
1082
1100
|
description=profile.description or "",
|
|
1083
1101
|
keywords=profile.keywords or None,
|
|
@@ -1086,7 +1104,8 @@ def _core_paths(profile: ServiceProfile) -> dict:
|
|
|
1086
1104
|
# Conformance response with required conformance classes
|
|
1087
1105
|
conformance_response = _R200_CONFORMANCE.copy()
|
|
1088
1106
|
if profile.required_conformance_classes:
|
|
1089
|
-
conformance_response["content"]["application/json"]["schema"]["properties"]["conformsTo"]["
|
|
1107
|
+
conformance_response["content"]["application/json"]["schema"]["properties"]["conformsTo"]["items"] = {
|
|
1108
|
+
"type": "string",
|
|
1090
1109
|
"enum": profile.required_conformance_classes
|
|
1091
1110
|
}
|
|
1092
1111
|
|
|
@@ -1305,7 +1324,7 @@ def build_openapi(profile: ServiceProfile) -> dict:
|
|
|
1305
1324
|
info["x-default-locale"] = profile.resource_default_locale
|
|
1306
1325
|
|
|
1307
1326
|
return {
|
|
1308
|
-
"openapi":
|
|
1327
|
+
"openapi": profile.openapi_version,
|
|
1309
1328
|
"info": info,
|
|
1310
1329
|
# Per OGC API - EDR Part 3 REQ_publishing: this profile is implementation-independent.
|
|
1311
1330
|
# The placeholder server URL "/" is required by OpenAPI validators but implementations
|
|
@@ -1348,6 +1367,7 @@ def build_openapi(profile: ServiceProfile) -> dict:
|
|
|
1348
1367
|
# ---------------------------------------------------------------------------
|
|
1349
1368
|
|
|
1350
1369
|
def build_asyncapi(profile: ServiceProfile) -> dict:
|
|
1370
|
+
version = profile.openapi_version
|
|
1351
1371
|
if not profile.pubsub:
|
|
1352
1372
|
raise ValueError("profile has no pubsub configuration")
|
|
1353
1373
|
|
|
@@ -1392,7 +1412,7 @@ def build_asyncapi(profile: ServiceProfile) -> dict:
|
|
|
1392
1412
|
"type": "object",
|
|
1393
1413
|
"required": ["type", "properties"],
|
|
1394
1414
|
"properties": {
|
|
1395
|
-
"type": {"type": "string", "
|
|
1415
|
+
"type": {"type": "string", **_const("Feature", version)},
|
|
1396
1416
|
"properties": {
|
|
1397
1417
|
"type": "object",
|
|
1398
1418
|
"required": ["id", "timestamp"],
|
|
@@ -1405,13 +1425,16 @@ def build_asyncapi(profile: ServiceProfile) -> dict:
|
|
|
1405
1425
|
}
|
|
1406
1426
|
}
|
|
1407
1427
|
|
|
1408
|
-
# Build servers
|
|
1409
|
-
|
|
1410
|
-
|
|
1428
|
+
# Build servers. When the profile lists explicit servers, use those
|
|
1429
|
+
# verbatim so it can describe its exact transport topology (WebSocket /ws,
|
|
1430
|
+
# AMQP 1.0, MQTT, ...). Otherwise synthesise a single implicit production
|
|
1431
|
+
# server from broker_host/broker_port/protocol.
|
|
1432
|
+
servers: dict = {}
|
|
1433
|
+
if not pub.servers:
|
|
1434
|
+
servers["production"] = {
|
|
1411
1435
|
"host": f"{pub.broker_host}:{pub.broker_port}",
|
|
1412
1436
|
"protocol": pub.protocol,
|
|
1413
1437
|
}
|
|
1414
|
-
}
|
|
1415
1438
|
for srv in pub.servers:
|
|
1416
1439
|
server_def = {
|
|
1417
1440
|
"description": srv.description,
|
|
@@ -1419,8 +1442,6 @@ def build_asyncapi(profile: ServiceProfile) -> dict:
|
|
|
1419
1442
|
"protocol": srv.protocol,
|
|
1420
1443
|
"security": [],
|
|
1421
1444
|
}
|
|
1422
|
-
if srv.port:
|
|
1423
|
-
server_def["variables"] = {"port": {"default": str(srv.port), "enum": [str(srv.port)]}}
|
|
1424
1445
|
if srv.pathname:
|
|
1425
1446
|
server_def["pathname"] = srv.pathname
|
|
1426
1447
|
servers[srv.name] = server_def
|
|
@@ -1497,7 +1518,7 @@ def _individual_test_adoc(profile: ServiceProfile, test_id: str) -> str:
|
|
|
1497
1518
|
f"identifier:: /conf/{profile.name}/{test.id}",
|
|
1498
1519
|
f"target:: /req/{profile.name}/{test.requirement_id}",
|
|
1499
1520
|
f"test-purpose:: Validate that {test.id.replace('-', ' ')} is correctly implemented.",
|
|
1500
|
-
"test-method::",
|
|
1521
|
+
f"test-method:: {test.method or ''}",
|
|
1501
1522
|
]
|
|
1502
1523
|
for step in test.steps:
|
|
1503
1524
|
lines.append(f"step:: {step}")
|
|
@@ -1600,7 +1621,7 @@ def _individual_test_adoc_for(profile: ServiceProfile, cls: str, test) -> str:
|
|
|
1600
1621
|
f"identifier:: /conf/{cls}/{test.id}",
|
|
1601
1622
|
f"target:: /req/{cls}/{test.requirement_id}",
|
|
1602
1623
|
f"test-purpose:: Validate that {test.id.replace('-', ' ')} is correctly implemented.",
|
|
1603
|
-
"test-method::",
|
|
1624
|
+
f"test-method:: {test.method or ''}",
|
|
1604
1625
|
]
|
|
1605
1626
|
for step in test.steps:
|
|
1606
1627
|
lines.append(f"step:: {step}")
|
{oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/models.py
RENAMED
|
@@ -261,6 +261,7 @@ class AbstractTest(BaseModel):
|
|
|
261
261
|
id: str # mirrors the requirement id it tests
|
|
262
262
|
requirement_id: str
|
|
263
263
|
steps: list[str] = Field(min_length=1)
|
|
264
|
+
method: str | None = Field(default=None, description="Optional test method description")
|
|
264
265
|
|
|
265
266
|
@model_validator(mode="after")
|
|
266
267
|
def ids_must_match(self) -> AbstractTest:
|
|
@@ -526,7 +527,14 @@ class CoverPage(BaseModel):
|
|
|
526
527
|
logo_y: int = Field(default=180, description="Logo y-offset from top of cover page")
|
|
527
528
|
tagline_font_size: int = Field(default=30, description="Font size of cover page tagline")
|
|
528
529
|
title_font_size: int = Field(default=54, description="Font size of cover page title")
|
|
529
|
-
|
|
530
|
+
edition_date_font_size: int = Field(
|
|
531
|
+
default=24,
|
|
532
|
+
description="Font size of the cover page edition and date lines (rendered a little smaller than the doc number)",
|
|
533
|
+
)
|
|
534
|
+
bold_edition: bool = Field(
|
|
535
|
+
default=False,
|
|
536
|
+
description="When true, render both the edition and date lines in bold font",
|
|
537
|
+
)
|
|
530
538
|
|
|
531
539
|
@field_validator("background", "text_color")
|
|
532
540
|
@classmethod
|
|
@@ -747,12 +755,18 @@ class DocumentMetadata(BaseModel):
|
|
|
747
755
|
|
|
748
756
|
|
|
749
757
|
class PubSubServer(BaseModel):
|
|
750
|
-
"""A single pub/sub server endpoint.
|
|
758
|
+
"""A single pub/sub server endpoint.
|
|
759
|
+
|
|
760
|
+
``protocol`` accepts the AsyncAPI protocol identifiers used by the EDR
|
|
761
|
+
Part 2 PubSub bindings: ``amqp`` (AMQP 0-9-1), ``amqp1`` (AMQP 1.0, e.g.
|
|
762
|
+
RabbitMQ 4.x native AMQP 1.0), ``mqtt``, ``kafka``, and the WebSocket
|
|
763
|
+
transports ``ws`` / ``wss``.
|
|
764
|
+
"""
|
|
751
765
|
name: str
|
|
752
766
|
description: str = ""
|
|
753
767
|
host: str
|
|
754
768
|
port: int | None = None
|
|
755
|
-
protocol: Literal["amqp", "mqtt", "kafka", "ws", "wss"] = "amqp"
|
|
769
|
+
protocol: Literal["amqp", "amqp1", "mqtt", "kafka", "ws", "wss"] = "amqp"
|
|
756
770
|
pathname: str | None = None
|
|
757
771
|
|
|
758
772
|
|
|
@@ -762,13 +776,22 @@ class CollectionPubSub(BaseModel):
|
|
|
762
776
|
|
|
763
777
|
|
|
764
778
|
class PubSubConfig(BaseModel):
|
|
765
|
-
"""Optional OGC API - EDR Part 2 (PubSub) configuration.
|
|
779
|
+
"""Optional OGC API - EDR Part 2 (PubSub) configuration.
|
|
780
|
+
|
|
781
|
+
When ``servers`` is populated the generated AsyncAPI uses those entries
|
|
782
|
+
verbatim (keyed by ``name``) and does not synthesise the implicit
|
|
783
|
+
``broker_host``/``broker_port`` ``production`` server, letting a profile
|
|
784
|
+
describe its exact transport topology (e.g. a native WebSocket ``/ws``
|
|
785
|
+
endpoint plus AMQP 1.0 and MQTT brokers). When ``servers`` is empty the
|
|
786
|
+
generator falls back to emitting a single ``production`` server built from
|
|
787
|
+
``broker_host``/``broker_port``/``protocol``.
|
|
788
|
+
"""
|
|
766
789
|
broker_host: str = "localhost"
|
|
767
790
|
broker_port: int = Field(default=5672, ge=1, le=65535)
|
|
768
|
-
protocol: Literal["amqp", "mqtt", "kafka"] = "amqp"
|
|
791
|
+
protocol: Literal["amqp", "amqp1", "mqtt", "kafka"] = "amqp"
|
|
769
792
|
collections: list[str] = Field(default_factory=list, description="Collection IDs that support PubSub")
|
|
770
793
|
filters: list[SubscriptionFilter] = Field(default_factory=list)
|
|
771
|
-
servers: list[PubSubServer] = Field(default_factory=list, description="
|
|
794
|
+
servers: list[PubSubServer] = Field(default_factory=list, description="Server endpoints (ws, wss, amqp, amqp1, mqtt); when set, replaces the implicit production server")
|
|
772
795
|
collection_filters: dict[str, CollectionPubSub] = Field(default_factory=dict, description="Per-collection filter overrides")
|
|
773
796
|
|
|
774
797
|
|
|
@@ -826,6 +849,10 @@ class ServiceProfile(BaseModel):
|
|
|
826
849
|
name: Annotated[str, Field(pattern=r"^[a-z0-9_]+$")]
|
|
827
850
|
title: str
|
|
828
851
|
version: str = "1.0"
|
|
852
|
+
openapi_version: Literal["3.0.0", "3.1.0"] = Field(
|
|
853
|
+
default="3.0.0",
|
|
854
|
+
description="OpenAPI version to generate (3.0.0 for compatibility, 3.1.0 for latest features)"
|
|
855
|
+
)
|
|
829
856
|
description: str | None = Field(
|
|
830
857
|
default=None,
|
|
831
858
|
description=(
|
{oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2/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.6.2
|
|
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
|
|
@@ -47,7 +47,7 @@ Dynamic: license-file
|
|
|
47
47
|
|
|
48
48
|
# OGC API Service Profile Builder
|
|
49
49
|
|
|
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.
|
|
50
|
+
Generate OGC API - EDR Part 3 Service Profile artifacts from a YAML config — OpenAPI 3.0.0/3.1.0, AsyncAPI, AsciiDoc requirements and conformance tests, and a publication-ready Metanorma PDF with optional custom (e.g. DGIWG) branding.
|
|
51
51
|
|
|
52
52
|
[](https://pypi.org/project/oapi-profile-builder/)
|
|
53
53
|
[](LICENSE)
|
|
@@ -57,7 +57,9 @@ Generate OGC API - EDR Part 3 Service Profile artifacts from a YAML config — O
|
|
|
57
57
|
## Quick Start
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
|
|
60
|
+
# Base install. Adds Pillow (cover image generation) with the [pdf] extra —
|
|
61
|
+
# required whenever your profile sets document_metadata.cover.logo.
|
|
62
|
+
pip install 'oapi-profile-builder[pdf]'
|
|
61
63
|
|
|
62
64
|
# Copy an example config and edit it
|
|
63
65
|
cp examples/minimal_profile.yaml my_profile.yaml
|
|
@@ -70,7 +72,7 @@ That's it. The `output/` directory will contain:
|
|
|
70
72
|
|
|
71
73
|
```
|
|
72
74
|
output/
|
|
73
|
-
├── openapi.yaml # OpenAPI 3.1.0 — ready for Swagger UI, Redoc, schemathesis
|
|
75
|
+
├── openapi.yaml # OpenAPI 3.0.0 or 3.1.0 — ready for Swagger UI, Redoc, schemathesis
|
|
74
76
|
├── profile_config.json # Round-trip serialized profile
|
|
75
77
|
├── document.adoc # Metanorma root document
|
|
76
78
|
├── sections/ # Abstract, Preface, Scope, Conformance, References, Terms
|
|
@@ -181,6 +183,30 @@ OGC API - EDR CITE Results
|
|
|
181
183
|
|
|
182
184
|
## GitHub Actions
|
|
183
185
|
|
|
186
|
+
### Self-service: generate artifacts from an issue (no fork, no install)
|
|
187
|
+
|
|
188
|
+
Anyone with a GitHub account can generate artifacts from their own config without cloning or installing anything:
|
|
189
|
+
|
|
190
|
+
1. Open a new issue using the **"Generate profile artifacts"** template ([New issue](../../issues/new/choose)).
|
|
191
|
+
2. Paste your profile YAML into the config field.
|
|
192
|
+
3. If your profile references local files (typically a cover logo via `document_metadata.cover.logo`), drag-and-drop them into the **"Additional assets (optional)"** field. Reference each uploaded file in your YAML as `uploads/<original-filename>` — for example:
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
document_metadata:
|
|
196
|
+
cover:
|
|
197
|
+
logo: uploads/my_logo.png
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The workflow downloads each dropped file into `./uploads/` before generation. It parses your YAML to figure out the target filename, so images that GitHub renders as an `<img>` tag with a UUID URL still land at the exact path your config references. Tip: to skip the upload entirely for the DGIWG logo, point at the copy that already ships in this repo: `logo: examples/assets/DGIWG_logo.png`.
|
|
201
|
+
4. Tick "Compile a PDF?" if you also want a Metanorma PDF, then submit.
|
|
202
|
+
5. A workflow validates the config, generates the artifacts, and comments on the issue with a link to download them as a zip (retained for 14 days).
|
|
203
|
+
|
|
204
|
+
The workflow only runs the generator over your declarative config — it never executes user-supplied code — and comments back on both success and failure. Don't paste secrets: the artifacts are attached to a public workflow run.
|
|
205
|
+
|
|
206
|
+
> Maintainers: create a `profile-request` label so requests are easy to filter (the workflow gates on a marker in the issue body, so it works even before the label exists).
|
|
207
|
+
|
|
208
|
+
### Use the action in your own workflow
|
|
209
|
+
|
|
184
210
|
No local install needed. Add this to any workflow to generate profile artifacts from a config file:
|
|
185
211
|
|
|
186
212
|
```yaml
|
|
@@ -239,7 +265,7 @@ Everything about the document — cover, title-page metadata, legal page, footer
|
|
|
239
265
|
| PDF colours (titles, tables, cover, backgrounds) | `document_metadata.colors` | `:presentation-metadata-color-*:` |
|
|
240
266
|
| Page-ii legal text (copyright / license / notice) | `document_metadata.boilerplate` | `:boilerplate-authority:` |
|
|
241
267
|
| Footer organisation name | `document_metadata.copyright_holder` | `:copyright-holder:` |
|
|
242
|
-
| Cover layout (logo size/position, tagline/title size, bold edition) | `document_metadata.cover.{logo_width, logo_y, tagline_font_size, title_font_size, bold_edition}` | Generated cover image |
|
|
268
|
+
| Cover layout (logo size/position, tagline/title/edition-date size, bold edition+date) | `document_metadata.cover.{logo_width, logo_y, tagline_font_size, title_font_size, edition_date_font_size, bold_edition}` | Generated cover image |
|
|
243
269
|
| Cover date | `document_metadata.doc_pub_date` | Long-format date on the generated cover + `:published-date:` |
|
|
244
270
|
| Remove the OGC logo from the legal page | `document_metadata.suppress_flavor_logo` | targeted `:pdf-stylesheet-override:` |
|
|
245
271
|
| Remove OGC design (crossing lines, circled numbers, rules, navy divider pages) | `document_metadata.suppress_design_elements` (or the individual `suppress_crossing_lines` / `plain_section_numbers` / `suppress_title_underlines`) | `:pdf-stylesheet-override:` + colour attributes |
|
|
@@ -352,6 +378,7 @@ abstract_tests: []
|
|
|
352
378
|
| `processes` | list | no | OGC API Processes to expose in the OpenAPI |
|
|
353
379
|
| `requirements` | list | no | Normative requirements for the AsciiDoc/PDF |
|
|
354
380
|
| `abstract_tests` | list | no | Conformance tests — each must reference a valid requirement `id` |
|
|
381
|
+
| `abstract_tests[].method` | string | no | Optional test method description |
|
|
355
382
|
| `pubsub` | object | no | OGC API - EDR Part 2 PubSub config — generates `asyncapi.yaml` |
|
|
356
383
|
| `collection_examples` | object | no | `{collectionId: {instanceId: "..."}}` — used by `validate-server` |
|
|
357
384
|
| `paging` | object | no | Features `/items` paging — adds a validated `limit` query parameter to the generated OpenAPI (see below) |
|
|
@@ -646,6 +673,7 @@ requirements:
|
|
|
646
673
|
abstract_tests:
|
|
647
674
|
- id: position-coveragejson # must equal requirement_id
|
|
648
675
|
requirement_id: position-coveragejson
|
|
676
|
+
method: Manual inspection of the /position response.
|
|
649
677
|
steps:
|
|
650
678
|
- Send GET /collections/{id}/position?coords=POINT(lon lat).
|
|
651
679
|
- Verify Content-Type is application/prs.coverage+json.
|
|
@@ -695,7 +723,7 @@ When present, generates `asyncapi.yaml` alongside `openapi.yaml`.
|
|
|
695
723
|
pubsub:
|
|
696
724
|
broker_host: my-broker.example.com
|
|
697
725
|
broker_port: 5672
|
|
698
|
-
protocol: amqp # amqp | mqtt | kafka
|
|
726
|
+
protocol: amqp # amqp | amqp1 | mqtt | kafka
|
|
699
727
|
collections:
|
|
700
728
|
- my_collection
|
|
701
729
|
filters:
|
|
@@ -704,6 +732,40 @@ pubsub:
|
|
|
704
732
|
type: string
|
|
705
733
|
```
|
|
706
734
|
|
|
735
|
+
`broker_host`/`broker_port`/`protocol` synthesise a single implicit
|
|
736
|
+
`production` AsyncAPI server. To describe an exact transport topology instead —
|
|
737
|
+
e.g. a native WebSocket `/ws` endpoint plus AMQP 1.0 and MQTT brokers, as the
|
|
738
|
+
`nws_connect_profile.yaml` example does — list them explicitly under `servers`.
|
|
739
|
+
When `servers` is set, the implicit `production` server is **not** emitted:
|
|
740
|
+
|
|
741
|
+
```yaml
|
|
742
|
+
pubsub:
|
|
743
|
+
broker_host: edr-broker
|
|
744
|
+
broker_port: 5672
|
|
745
|
+
protocol: amqp1 # AMQP 1.0 (e.g. RabbitMQ 4.x native)
|
|
746
|
+
collections: [water_gauge, wwa]
|
|
747
|
+
servers:
|
|
748
|
+
- name: websocket_secure
|
|
749
|
+
description: Native WebSocket live map (production)
|
|
750
|
+
host: ''
|
|
751
|
+
protocol: wss # amqp | amqp1 | mqtt | kafka | ws | wss
|
|
752
|
+
pathname: /ws
|
|
753
|
+
- name: amqp
|
|
754
|
+
description: AMQP 1.0 broker (RabbitMQ 4.x native AMQP 1.0)
|
|
755
|
+
host: 'edr-broker:5672'
|
|
756
|
+
protocol: amqp1
|
|
757
|
+
- name: mqtt
|
|
758
|
+
description: MQTT broker (RabbitMQ MQTT plugin)
|
|
759
|
+
host: 'edr-broker:1883'
|
|
760
|
+
protocol: mqtt
|
|
761
|
+
collection_filters: # per-collection filter overrides
|
|
762
|
+
water_gauge:
|
|
763
|
+
filters:
|
|
764
|
+
- name: flood_stages
|
|
765
|
+
description: Alert for selected flood stages
|
|
766
|
+
type: array
|
|
767
|
+
```
|
|
768
|
+
|
|
707
769
|
---
|
|
708
770
|
|
|
709
771
|
### `document_metadata`
|
|
@@ -764,7 +826,9 @@ Two `document_metadata` blocks let you rebrand the PDF (e.g. for DGIWG) without
|
|
|
764
826
|
document_metadata:
|
|
765
827
|
doc_number: "DGIWG 134"
|
|
766
828
|
cover:
|
|
767
|
-
logo: assets/
|
|
829
|
+
logo: examples/assets/DGIWG_logo.png # path relative to the working directory (or absolute).
|
|
830
|
+
# From the self-service issue form use uploads/<file>
|
|
831
|
+
# instead and drag-drop the file into "Additional assets".
|
|
768
832
|
tagline: Delivering Military Advantage through multi-national geospatial interoperability
|
|
769
833
|
background: "#FFFFFF" # optional cover background (default white)
|
|
770
834
|
text_color: "#1F3864" # optional cover text colour (default black)
|
|
@@ -773,11 +837,12 @@ document_metadata:
|
|
|
773
837
|
font_italic: "Source Sans Pro" # optional italic face for the tagline
|
|
774
838
|
watermark: "DRAFT" # optional diagonal watermark stamped on the cover
|
|
775
839
|
# --- cover layout (all optional) ---
|
|
776
|
-
logo_width:
|
|
840
|
+
logo_width: 300 # target logo width in px (default 420)
|
|
777
841
|
logo_y: 100 # logo vertical offset from top in px (default 180)
|
|
778
842
|
tagline_font_size: 20 # tagline point size (default 30)
|
|
779
843
|
title_font_size: 40 # title point size (default 54)
|
|
780
|
-
|
|
844
|
+
edition_date_font_size: 22 # edition/date point size (default 24)
|
|
845
|
+
bold_edition: true # render the "Edition X" and "Dated ..." lines in bold (default false)
|
|
781
846
|
colors:
|
|
782
847
|
text: "#000000" # body text
|
|
783
848
|
cover_text: "#1F3864" # cover text / section numbers / ToC
|
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/cite_features.py
RENAMED
|
File without changes
|
|
File without changes
|
{oapi_profile_builder-3.5.0 → oapi_profile_builder-3.6.2}/src/oapi_profile_builder/compile.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|