Metadata-Version: 2.4
Name: oapi-profile-builder
Version: 2.0.3
Summary: Authoritative tooling for creating OGC API Service Profiles (EDR, Features)
Author-email: Shane Mill <shane.mill@noaa.gov>
License: Apache License
        Version 2.0, January 2004
        http://www.apache.org/licenses/
        
        Copyright 2025 NOAA/NWS/Meteorological Development Laboratory
        
        Licensed under the Apache License, Version 2.0 (the "License");
        you may not use this file except in compliance with the License.
        You may obtain a copy of the License at
        
            http://www.apache.org/licenses/LICENSE-2.0
        
        Unless required by applicable law or agreed to in writing, software
        distributed under the License is distributed on an "AS IS" BASIS,
        WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
        See the License for the specific language governing permissions and
        limitations under the License.
        
Project-URL: Homepage, https://github.com/ShaneMill1/OGC-API-Service-Profile-Builder
Project-URL: Issues, https://github.com/ShaneMill1/OGC-API-Service-Profile-Builder/issues
Keywords: ogc,edr,features,environmental-data-retrieval,profile,openapi,asyncapi
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: GIS
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: edr-pydantic>=0.3
Requires-Dist: pydantic>=2.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.31
Provides-Extra: validate
Requires-Dist: schemathesis>=3.0; extra == "validate"
Dynamic: license-file

# OGC API Service Profile Builder

Authoritative tooling for creating OGC API Service Profiles (EDR, Features), built on Pydantic and [edr-pydantic](https://github.com/KNMI/edr-pydantic).

## Overview

Profile structure is defined as Pydantic models (`src/oapi_profile_builder/models.py`). Instantiating a `ServiceProfile` validates the entire profile — cross-model validators catch referential errors — before any files are written.

Collections use `edr-pydantic`'s authoritative `Collection` model directly, meaning a profile config is simultaneously a valid EDR collection descriptor and a Part 3 profile definition.

## Installation

```bash
pip install oapi-profile-builder
```

---

## Workflow

<img width="1001" height="721" alt="OGC API Service Profile Builder - Pydantic Validation Architecture drawio" src="https://github.com/user-attachments/assets/092c3dfc-549e-41b0-8a92-af0b89689950" />


### 1. Author a Profile Config

A profile config is a YAML or JSON file. Start with the minimal example:

```bash
cp examples/minimal_profile.yaml my_profile.yaml
```

The minimal valid config:

```yaml
name: my_profile
title: My EDR Profile

# OGC API - EDR Part 3 compliance fields (recommended)
required_conformance_classes:
  - "http://www.opengis.net/spec/ogcapi-edr-1/1.0/conf/core"

extent_requirements:
  minimum_bbox: [-180, -90, 180, 90]
  allowed_crs:
    - "http://www.opengis.net/def/crs/OGC/1.3/CRS84"

output_formats:
  - name: GeoJSON
    media_type: application/geo+json
    schema_ref: https://geojson.org/schema/FeatureCollection.json

collections:
  - id: my_collection
    links:
      - href: https://example.com/collections/my_collection
        rel: self
        type: application/json
    extent:
      spatial:
        bbox:
          - [-180, -90, 180, 90]
        crs: "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
    parameter_names:
      temp:
        type: Parameter
        observedProperty:
          label: Temperature
        unit:              # REQUIRED per OGC API - EDR Part 3
          label: Celsius
          symbol: C

# Example requirement and abstract test for asciidoc/PDF
requirements:
  - id: items-endpoint
    statement: The service SHALL provide a /collections/water_gauge/items endpoint.
    parts:
      - The service SHALL return GeoJSON FeatureCollection.
      - Each feature SHALL include gauge_height property.

abstract_tests:
  - id: items-endpoint
    requirement_id: items-endpoint
    steps:
      - Send GET request to /collections/water_gauge/items.
      - Verify response Content-Type is application/geo+json.
      - Verify each feature contains gauge_height property.
```

See [`examples/minimal_profile.yaml`](examples/minimal_profile.yaml) for a complete working example and [`examples/nwsviz_profile.yaml`](examples/nwsviz_profile.yaml) for a full profile with 13 collections, 3 processes, requirements, abstract tests, and document metadata.

### 2. Generate Profile Artifacts

```bash
oapi-profile-builder generate \
  --config my_profile.yaml \
  --output ./my_profile
```

Produces:

```
my_profile/
├── openapi.yaml
├── profile_config.json
├── document.adoc                        # Metanorma root document
├── sections/
│   ├── 00-abstract.adoc
│   ├── 01-preface.adoc
│   ├── 02-scope.adoc
│   ├── 03-conformance.adoc
│   ├── 04-references.adoc
│   ├── 05-terms.adoc
│   ├── 06-requirements.adoc
│   └── 07-abstract-tests.adoc
├── requirements/
│   ├── requirements_class_core.adoc
│   └── core/REQ_<id>.adoc
└── abstract_tests/
    ├── ATS_class_core.adoc
    └── core/ATS_<id>.adoc
```

Validate a config without generating output:

```bash
oapi-profile-builder validate --config my_profile.yaml
```

### 3. Compile OGC PDF

Requires Docker. Shells out to the official `metanorma/metanorma` image — no Ruby or LaTeX install needed.

```bash
oapi-profile-builder generate \
  --config my_profile.yaml \
  --output ./my_profile \
  --pdf
```

The `document_metadata` block in the profile config drives the Metanorma document header:

```yaml
document_metadata:
  doc_number: "24-nwsviz"
  doc_subtype: implementation
  copyright_year: 2026
  editors:
    - Shane Mill
  submitting_orgs:
    - NOAA/NWS/MDL
  keywords:
    - ogcdoc
    - OGC API
    - EDR
    - NWSViz
    - service profile
  external_id: http://www.opengis.net/doc/dp/ogcapi-edr-nwsviz/1.0
```

Produces `my_profile/document.pdf` — a fully compliant OGC `draft-standard` PDF with Abstract, Preface, Scope, Conformance, References, Terms, Requirements class, and normative Abstract Test Suite annex.

### 4. Validate Against a Live Server

```bash
oapi-profile-builder validate-server \
  --config my_profile.yaml \
  --url https://edr-api-desi-c.mdl.nws.noaa.gov \
  --max-examples 3
```

Results:

```
Operations:  100 selected / 106 total
Tested:      47
Test cases:  1002 generated, 1002 passed

No issues found in 49.51s
```

Use `--stateful` to additionally test job lifecycle endpoints (`/jobs/{jobId}`, `DELETE /jobs/{jobId}`) via POST `/execution` chaining.

Add `collection_examples` to your config to supply real `instanceId` values for schemathesis path parameters:

```yaml
collection_examples:
  my_collection:
    instanceId: "2025-04-02T00:00:00Z"
```

### 5. OGC CITE Conformance Testing

#### EDR Conformance Testing

Run the official OGC API - EDR Part 1 conformance test suite (ets-ogcapi-edr10):

```bash
oapi-profile-builder cite-test \
  --url https://edr-api-desi-c.mdl.nws.noaa.gov \
  --report ./cite_results
```

Results:

```
OGC API - EDR CITE Results
  Passed:  76/84
  Failed:  0
  Skipped: 8

✓ All CITE tests passed.
```

The tool automatically:
- Clones and builds ets-ogcapi-edr10 from GitHub on first run
- Caches Docker image (`ogccite/ets-ogcapi-edr10:local`) for subsequent runs
- Runs TestNG tests via `docker exec`
- Supports localhost testing with `--network host` mode
- Generates JSON report with detailed test results

The skipped tests are optional features not implemented by the server.

#### Features Conformance Testing

Run the official OGC API - Features Part 1 conformance test suite (ets-ogcapi-features10):

```bash
oapi-profile-builder cite-test-features \
  --url https://api.example.com \
  --report ./cite_features_results
```

Results:

```
OGC API - Features CITE Results
  Passed:  639/712
  Failed:  18
  Skipped: 55

✗ 18 test(s) failed.
```

The tool automatically:
- Pulls pre-built Docker image (`ogccite/ets-ogcapi-features10:latest`) from Docker Hub
- Runs TestNG tests via `docker exec`
- Supports localhost testing with `--network host` mode
- Generates JSON report with detailed test results

The skipped tests are optional features not implemented by the server.

---

## Profile Configuration Guide

This section explains what is and isn't allowed when creating a profile, and how the tool validates your configuration.

### What Gets Validated

When you run `generate` or `validate`, the tool instantiates a `ServiceProfile` Pydantic model that enforces all of the following rules before any files are written. If any rule is violated, you get a clear error message pointing to the offending field.

#### Profile-Level Fields

| Field | Rules |
|---|---|
| `name` | Must match `^[a-z0-9_]+$` — lowercase letters, digits, and underscores only. Used in OGC URIs. |
| `title` | Any non-empty string. |
| `version` | Any string. Defaults to `"1.0"`. |
| `collections` | At least one collection is required. No duplicate `id` values. |

#### Collection IDs

By default, collection IDs can be any string accepted by edr-pydantic. To enforce a naming convention across all collections, use `collection_id_pattern`:

```yaml
# Only allow lowercase snake_case collection IDs
collection_id_pattern: "^[a-z][a-z0-9_]*$"
```

The pattern is matched using Python's `re.fullmatch()`, so it must match the **entire** ID string.

#### CRS, TRS, and VRS Constraints

Each collection declares a CRS in `extent.spatial.crs`, and optionally a TRS in `extent.temporal.trs` and VRS in `extent.vertical.vrs`. The profile can constrain these values in two ways:

**Enumerated list** — only the exact values listed are accepted:

```yaml
extent_requirements:
  minimum_bbox: [-180, -90, 180, 90]
  allowed_crs:
    - "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
    - "http://www.opengis.net/def/crs/EPSG/0/4326"
```

**Regex pattern** — any value matching the pattern is accepted:

```yaml
extent_requirements:
  minimum_bbox: [-180, -90, 180, 90]
  # Accept any OGC or EPSG CRS
  crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
```

If both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both**. At least one of `allowed_crs` or `crs_pattern` is required when `extent_requirements` is present.

The same enum/regex approach works for TRS (`allowed_trs` / `trs_pattern`) and VRS (`allowed_vrs` / `vrs_pattern`).

#### Parameter Name Constraints

By default, parameter names (the keys in `parameter_names`) can be any string. To enforce a naming convention, use `parameter_name_pattern`:

```yaml
# CF-style lowercase parameter names
parameter_name_pattern: "^[a-z][a-z0-9_]*$"
```

```yaml
# Allow uppercase abbreviations like WMO codes
parameter_name_pattern: "^[A-Za-z][A-Za-z0-9_]*$"
```

Every key in every collection's `parameter_names` must match this pattern. The pattern uses `re.fullmatch()`.

Additionally, per OGC API - EDR Part 3, every parameter must specify both `unit` and `observedProperty`. The tool enforces this automatically.

#### Requirement and Test IDs

| Field | Rules |
|---|---|
| Requirement `id` | Must match `^[a-z0-9][a-z0-9\-]*$` — lowercase, digits, hyphens. Cannot end with a hyphen. |
| AbstractTest `id` | Must exactly equal its `requirement_id`. |
| AbstractTest `requirement_id` | Must reference an existing requirement `id`. |

#### What Happens When Validation Fails

The tool prints a Pydantic validation error with the field path and a human-readable message. For example:

```
Value error, Collection 'my_data' CRS 'urn:ogc:def:crs:EPSG::4326'
does not match crs_pattern '^http://www\.opengis\.net/def/crs/(OGC|EPSG)/.*$'
```

```
Value error, Parameter name 'WIND_SPEED' in collection 'weather'
does not match parameter_name_pattern '^[a-z][a-z0-9_]*$'
```

```
Value error, Collection id 'My-Collection' does not match
collection_id_pattern '^[a-z][a-z0-9_]*$'
```

### How Patterns Flow Into the Generated OpenAPI

When you specify `crs_pattern`, `allowed_crs`, or `parameter_name_pattern`, those constraints are embedded in the generated `openapi.yaml` so that runtime validation tools can enforce them:

- `crs_pattern` → `pattern` on the CRS string schema in collection responses
- `allowed_crs` → `enum` on the CRS string schema
- `trs_pattern` / `allowed_trs` → `pattern` / `enum` on the TRS field in extent.temporal
- `vrs_pattern` / `allowed_vrs` → `pattern` / `enum` on the VRS field in extent.vertical
- `parameter_name_pattern` → `propertyNames.pattern` on the `parameter_names` object schema

This means schemathesis, CITE tests, and client SDKs can validate server responses against these constraints without needing access to the original profile YAML.

### Quick Reference: Regex Examples

| Use Case | Pattern |
|---|---|
| Only OGC CRS84 | `^http://www\\.opengis\\.net/def/crs/OGC/1\\.3/CRS84$` |
| Any OGC or EPSG CRS | `^http://www\\.opengis\\.net/def/crs/(OGC\|EPSG)/.*$` |
| Any valid CRS URI | `^http://www\\.opengis\\.net/def/crs/.*$` |
| ISO-8601 TRS family | `^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$` |
| Lowercase snake_case names | `^[a-z][a-z0-9_]*$` |
| CF standard name style | `^[a-z][a-z0-9_]*(_[a-z0-9]+)*$` |
| WMO-style alphanumeric | `^[A-Za-z][A-Za-z0-9_]*$` |
| Lowercase with hyphens | `^[a-z][a-z0-9\\-]*$` |

---

## Config Reference

### Top-level fields

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | Lowercase identifier using only `a-z`, `0-9`, `_`. Used in OGC URIs and OpenAPI `operationId`s. e.g. `water_gauge` |
| `title` | `string` | yes | Human-readable profile title |
| `version` | `string` | no | Profile version. Defaults to `1.0` |
| `server_url` | `string` | no | Base URL of the live server (for documentation only - not used in profile OpenAPI per OGC API - EDR Part 3) |
| `collections` | `list` | yes | One or more EDR collections (see below) |
| `processes` | `list` | no | OGC API Processes to include in the OpenAPI (see below) |
| `requirements` | `list` | no | Normative requirements (see below) |
| `abstract_tests` | `list` | no | Conformance tests — each must reference a valid requirement `id` (see below) |
| `pubsub` | `object` | no | OGC API - EDR Part 2 PubSub configuration (see below) |
| `collection_examples` | `object` | no | Map of collection id → example parameter values (e.g. `instanceId`) for server validation |
| `document_metadata` | `object` | no | Metanorma document header fields for PDF compilation (see below) |
| `required_conformance_classes` | `list[string]` | no | Conformance classes that implementations must declare. Defaults to EDR Core |
| `extent_requirements` | `object` | no | Profile-level extent restrictions (see below) |
| `output_formats` | `list` | no | Profile-level output format definitions with schema references (see below) |
| `collection_id_pattern` | `string` | no | Regex pattern that all collection IDs must match (validated at build time) |
| `parameter_name_pattern` | `string` | no | Regex pattern that all `parameter_names` keys must match (validated at build time) |

---

### `collections[]`

Each collection uses the [edr-pydantic](https://github.com/KNMI/edr-pydantic) `Collection` schema — the same model an EDR server returns at `/collections/{id}`. Key fields:

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Collection identifier |
| `title` | `string` | no | Human-readable collection name |
| `description` | `string` | no | Collection description |
| `links` | `list` | yes | At minimum a `self` link |
| `extent.spatial.bbox` | `list` | yes | Bounding box as `[[minLon, minLat, maxLon, maxLat]]` |
| `extent.spatial.crs` | `string` | yes | CRS URI, typically `http://www.opengis.net/def/crs/OGC/1.3/CRS84` |
| `data_queries` | `object` | no | Which EDR query types this collection supports |
| `output_formats` | `list` | no | Supported output format labels e.g. `GeoJSON`, `CoverageJSON` |
| `parameter_names` | `object` | no | Map of parameter id → `Parameter` object |

#### `data_queries`

Supported keys: `items` · `position` · `area` · `radius` · `cube` · `trajectory` · `corridor` · `locations` · `instances`

```yaml
data_queries:
  position:
    link:
      href: https://example.com/collections/water_gauge/position
      rel: data
      variables:
        query_type: position
        output_formats:
          - CoverageJSON
  items:
    link:
      href: https://example.com/collections/water_gauge/items
      rel: data
      variables:
        query_type: items
        output_formats:
          - GeoJSON
```

#### `parameter_names`

**Note:** Per OGC API - EDR Part 3 requirements, all parameters must specify `unit` and `observedProperty`. The tool validates this automatically.

```yaml
parameter_names:
  gauge_height:
    type: Parameter
    observedProperty:
      label: Gauge Height
    unit:
      label: feet
      symbol: ft
```

---

### `extent_requirements`

Profile-level extent restrictions per OGC API - EDR Part 3 REQ_extent. These constraints are **enforced at profile build time** — if any collection's CRS, TRS, or VRS value violates the rules here, the profile will be rejected with a clear error message. The constraints are also **embedded in the generated OpenAPI** so that downstream tools (schemathesis, CITE tests, client SDKs) can enforce them at runtime.

| Field | Type | Required | Description |
|---|---|---|---|
| `minimum_bbox` | `list[float]` | yes | Minimum spatial bounds `[minLon, minLat, maxLon, maxLat]` |
| `allowed_crs` | `list[string]` | no | Enumerated list of valid CRS values |
| `crs_pattern` | `string` | no | Regular expression defining valid CRS string patterns |
| `allowed_trs` | `list[string]` | no | Enumerated list of valid TRS values |
| `trs_pattern` | `string` | no | Regular expression defining valid TRS string patterns |
| `allowed_vrs` | `list[string]` | no | Enumerated list of valid VRS values |
| `vrs_pattern` | `string` | no | Regular expression defining valid VRS string patterns |

**Note:** Either `allowed_crs` or `crs_pattern` must be specified.

**Enum approach** — lock down to specific values:

```yaml
extent_requirements:
  minimum_bbox: [-180, -90, 180, 90]
  allowed_crs:
    - "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
    - "http://www.opengis.net/def/crs/EPSG/0/4326"
  allowed_trs:
    - "http://www.opengis.net/def/uom/ISO-8601/0/Gregorian"
```

**Regex approach** — allow any CRS from a family:

```yaml
extent_requirements:
  minimum_bbox: [-180, -90, 180, 90]
  # Accept any OGC or EPSG CRS
  crs_pattern: "^http://www\\.opengis\\.net/def/crs/(OGC|EPSG)/.*$"
  # Accept any ISO-8601 TRS
  trs_pattern: "^http://www\\.opengis\\.net/def/uom/ISO-8601/.*$"
```

Both approaches can coexist — if both `allowed_crs` and `crs_pattern` are specified, a collection's CRS must satisfy **both** constraints.

---

### `output_formats[]`

Profile-level output format definitions with schema references per OGC API - EDR Part 3 REQ_output-format.

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | `string` | yes | Format name (e.g., GeoJSON, CoverageJSON) |
| `media_type` | `string` | yes | MIME type (e.g., application/geo+json) |
| `schema_ref` | `string` | no | URL to schema definition |

```yaml
output_formats:
  - name: GeoJSON
    media_type: application/geo+json
    schema_ref: https://geojson.org/schema/FeatureCollection.json
  - name: CoverageJSON
    media_type: application/prs.coverage+json
    schema_ref: https://schemas.opengis.net/ogcapi/edr/1.0/openapi/schemas/coverageJSON.yaml
```

---

### `required_conformance_classes[]`

Conformance classes that implementations must declare at `/conformance` per OGC API - EDR Part 3 REQ_api.

Defaults to:
```yaml
required_conformance_classes:
  - "http://www.opengis.net/spec/ogcapi-edr-1/1.0/conf/core"
```

Add additional conformance classes as needed:
```yaml
required_conformance_classes:
  - "http://www.opengis.net/spec/ogcapi-edr-1/1.0/conf/core"
  - "http://www.opengis.net/spec/ogcapi-edr-1/1.0/conf/oas30"
  - "http://www.opengis.net/spec/ogcapi-edr-2/1.0/conf/pubsub"
```

---

### `processes[]`

OGC API Processes to expose in the generated OpenAPI. Each entry produces `/processes/{id}` and `/processes/{id}/execution` paths, plus `/processes`, `/jobs`, `/jobs/{jobId}`, and `/jobs/{jobId}/results`.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Process identifier e.g. `edr-zarr-difference` |
| `title` | `string` | no | Human-readable process name |
| `description` | `string` | no | Process description |
| `output_content` | `object` | no | OpenAPI content map for the 200 response. Defaults to `application/json` |

```yaml
processes:
  - id: edr-zarr-difference
    title: EDR Zarr Dataset Difference
    description: Calculates the difference between two EDR Zarr datasets.
    output_content:
      application/zip:
        schema:
          type: object
```

---

### `requirements[]`

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Lowercase, hyphen-separated. Must match `^[a-z0-9][a-z0-9\-]*$` |
| `statement` | `string` | yes | One-sentence normative statement |
| `parts` | `list[string]` | yes | One or more SHALL/MUST clauses |

```yaml
requirements:
  - id: position-coveragejson
    statement: The position query SHALL return CoverageJSON.
    parts:
      - The service SHALL provide a /collections/{id}/position endpoint.
      - The response Content-Type SHALL be application/prs.coverage+json.
```

---

### `abstract_tests[]`

Every `requirement_id` must match an existing requirement `id` — the model validator will reject the profile if not.

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | `string` | yes | Must equal `requirement_id` |
| `requirement_id` | `string` | yes | The `id` of the requirement this test validates |
| `steps` | `list[string]` | yes | Ordered test steps |

```yaml
abstract_tests:
  - id: position-coveragejson
    requirement_id: position-coveragejson
    steps:
      - Send GET request to /collections/{id}/position?coords=POINT(lon lat).
      - Verify the response Content-Type is application/prs.coverage+json.
```

---

### `pubsub`

When present, an `asyncapi.yaml` is generated.

| Field | Type | Default | Description |
|---|---|---|---|
| `broker_host` | `string` | `localhost` | Message broker hostname |
| `broker_port` | `integer` | `5672` | Broker port (1–65535) |
| `protocol` | `string` | `amqp` | One of `amqp`, `mqtt`, `kafka` |
| `filters` | `list` | `[]` | Subscription filters |

Each filter: `name` (required), `description` (required), `type` (one of `string`, `number`, `array`, `boolean`, default `string`).

---

### `document_metadata`

Controls the Metanorma document header when compiling a PDF with `--pdf`.

| Field | Type | Required | Description |
|---|---|---|---|
| `doc_number` | `string` | yes | OGC document number e.g. `24-nwsviz` |
| `doc_subtype` | `string` | yes | One of `implementation`, `best-practice`, `engineering-report` |
| `editors` | `list[string]` | yes | Editor names |
| `submitting_orgs` | `list[string]` | yes | Submitting organization names |
| `keywords` | `list[string]` | no | Document keywords |
| `copyright_year` | `integer` | no | Defaults to current year |
| `external_id` | `string` | no | OGC external document URI |

---

## OGC API - EDR Part 3 Compliance

This tool implements the requirements of OGC API - EDR Part 3: Service Profiles (draft standard):

### Key Compliance Features

1. **Profile OpenAPI Document** (REQ_publishing)
   - Generated OpenAPI has empty `servers` array (profile is implementation-independent)
   - Landing page schema requires `profile` link relation
   - Profile URI advertised in `x-ogc-profile` info field

2. **Conformance Classes** (REQ_api)
   - `/conformance` endpoint schema specifies required conformance classes
   - Defaults to EDR Core, customizable via `required_conformance_classes`

3. **Parameter Names** (REQ_parameter-names)
   - Validates that all parameters specify `unit` and `observedProperty`
   - Automatically enforced during profile validation

4. **Extent Requirements** (REQ_extent)
   - Profile-level `extent_requirements` specify minimum bounds
   - CRS/TRS/VRS restrictions via enumerated lists or regex patterns
   - **Enforced at build time**: collection CRS/TRS/VRS values are validated against `allowed_*` lists and `*_pattern` regexes
   - **Propagated to OpenAPI**: constraints appear as `enum` or `pattern` in the generated collection response schemas

5. **Parameter Names** (REQ_parameter-names)
   - Validates that all parameters specify `unit` and `observedProperty`
   - Optional `parameter_name_pattern` enforces naming conventions across all collections
   - Pattern constraints are embedded in the generated OpenAPI as `propertyNames.pattern`

6. **Collection ID Pattern**
   - Optional `collection_id_pattern` enforces naming conventions for collection IDs
   - Validated at build time via `re.fullmatch()`

7. **Output Formats** (REQ_output-format)
   - Profile-level `output_formats` with schema references
   - Links to JSON Schema, XML Schema, or format specifications

8. **Pub/Sub** (REQ_pubsub)
   - Automatically adds Part 2 conformance requirement when `pubsub` is present
   - AsyncAPI document specifies channels and payloads

### Profile Types

The tool supports both:
- **Class 1 Profiles**: Restrictive profiles that constrain EDR Core
- **Class 2 Profiles**: Extended profiles that add custom requirements (e.g., processes)

Both remain compliant with EDR Core - extensions are optional, not mandatory.

---

## Programmatic Use

```python
from oapi_profile_builder.models import ServiceProfile
from oapi_profile_builder.generate import generate
from pathlib import Path

profile = ServiceProfile.model_validate(config_dict)
generate(profile, Path("./output"))
```

## Repository Structure

```
├── src/
│   └── oapi_profile_builder/
│       ├── models.py            # Authoritative Pydantic schema
│       ├── generate.py          # Validated model → OpenAPI, AsyncAPI, AsciiDoc
│       ├── compile.py           # PDF compilation via metanorma/metanorma Docker image
│       ├── cite.py              # OGC CITE test suite orchestration
│       └── cli.py               # CLI entry point
├── examples/
│   ├── water_gauge.yaml         # Minimal example profile config
│   └── nwsviz_profile.yaml      # Full NWSViz profile: 13 collections, 3 processes, PDF metadata
├── profile.schema.json          # Machine-readable JSON Schema for profile configs
└── pyproject.toml
```

## Standards

- OGC API - EDR Part 1: Core
- OGC API - EDR Part 2: PubSub
- OGC API - EDR Part 3: Service Profiles (draft)
- OGC API - Processes Part 1
- OpenAPI 3.0 / AsyncAPI 3.0
- Metanorma/AsciiDoc documentation format

## License

Apache — See [LICENSE](LICENSE) for details.

## Contact

- **Author**: Shane Mill (NOAA/NWS/MDL)
- **Email**: shane.mill@noaa.gov
- **Issues**: https://github.com/ShaneMill1/OGC-API-Service-Profile-Builder/issues
