oapi-profile-builder 3.3.0__tar.gz → 3.4.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.
Files changed (20) hide show
  1. {oapi_profile_builder-3.3.0/src/oapi_profile_builder.egg-info → oapi_profile_builder-3.4.0}/PKG-INFO +1 -1
  2. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/pyproject.toml +1 -1
  3. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder/cover.py +11 -7
  4. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder/generate.py +215 -3
  5. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder/models.py +35 -5
  6. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0/src/oapi_profile_builder.egg-info}/PKG-INFO +1 -1
  7. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/LICENSE +0 -0
  8. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/README.md +0 -0
  9. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/setup.cfg +0 -0
  10. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder/__init__.py +0 -0
  11. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder/cite.py +0 -0
  12. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder/cite_features.py +0 -0
  13. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder/cli.py +0 -0
  14. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder/compile.py +0 -0
  15. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder/server_validation.py +0 -0
  16. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder.egg-info/SOURCES.txt +0 -0
  17. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder.egg-info/dependency_links.txt +0 -0
  18. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder.egg-info/entry_points.txt +0 -0
  19. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder.egg-info/requires.txt +0 -0
  20. {oapi_profile_builder-3.3.0 → oapi_profile_builder-3.4.0}/src/oapi_profile_builder.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oapi-profile-builder
3
- Version: 3.3.0
3
+ Version: 3.4.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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "oapi-profile-builder"
7
- version = "3.3.0"
7
+ version = "3.4.0"
8
8
  description = "Authoritative tooling for creating OGC API Service Profiles (EDR, Features)"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -173,12 +173,12 @@ def build_cover_image(profile, output_dir: Path) -> str | None:
173
173
  draw.text(((_PAGE_W - tw) / 2, y), text, font=font, fill=fill)
174
174
  return y + th
175
175
 
176
- y = 180
176
+ y = getattr(cover, "logo_y", 180)
177
177
 
178
178
  # --- Logo (centered, scaled to a target width) ---
179
179
  logo = Image.open(logo_path).convert("RGBA")
180
- target_logo_w = 420 # desired cover logo width
181
- max_logo_w = 520 # hard cap
180
+ target_logo_w = getattr(cover, "logo_width", 420)
181
+ max_logo_w = max(520, target_logo_w)
182
182
  max_upscale = 3.0 # don't upscale tiny logos beyond this (avoids heavy blur)
183
183
  desired = min(target_logo_w, int(logo.width * max_upscale), max_logo_w)
184
184
  if logo.width != desired:
@@ -190,7 +190,8 @@ def build_cover_image(profile, output_dir: Path) -> str | None:
190
190
 
191
191
  # --- Tagline (italic) ---
192
192
  if cover.tagline:
193
- y = centered(cover.tagline, fonts.italic_font(30), y, fg) + 60
193
+ tagline_fs = getattr(cover, "tagline_font_size", 30)
194
+ y = centered(cover.tagline, fonts.italic_font(tagline_fs), y, fg) + 60
194
195
 
195
196
  # --- Document number ---
196
197
  if m and m.doc_number:
@@ -199,16 +200,19 @@ def build_cover_image(profile, output_dir: Path) -> str | None:
199
200
  # --- Edition ---
200
201
  edition = getattr(profile, "version", None)
201
202
  if edition:
202
- y = centered(f"Edition {edition}", fonts.regular_font(28), y, fg) + 24
203
+ bold_ed = bool(getattr(cover, "bold_edition", False))
204
+ y = centered(f"Edition {edition}", fonts.regular_font(28, bold=bold_ed), y, fg) + 24
203
205
 
204
206
  # --- Date ---
205
- date_str = _format_date(m.publication_date if m else None)
207
+ pub_date = (m.doc_pub_date or m.publication_date) if m else None
208
+ date_str = _format_date(pub_date)
206
209
  if date_str:
207
210
  y = centered(f"Dated {date_str}", fonts.regular_font(28), y, fg) + 80
208
211
 
209
212
  # --- Title (large, bold, wrapped) ---
210
213
  title = getattr(profile, "title", "") or ""
211
- title_font = fonts.regular_font(54, bold=True)
214
+ title_fs = getattr(cover, "title_font_size", 54)
215
+ title_font = fonts.regular_font(title_fs, bold=True)
212
216
  for line in _wrap(draw, title, title_font, _PAGE_W - 240):
213
217
  y = centered(line, title_font, y, fg) + 16
214
218
 
@@ -761,6 +761,35 @@ def _params_with_default_f(params: list[dict], profile: "ServiceProfile | None",
761
761
  return [f_param if p is _F else p for p in params]
762
762
 
763
763
 
764
+ def _paging_limit_param(profile: "ServiceProfile | None") -> dict | None:
765
+ """Return the `limit` query parameter for feature paging, or None when disabled."""
766
+ if not (profile and profile.paging and profile.paging.enabled):
767
+ return None
768
+ return {
769
+ "name": "limit",
770
+ "in": "query",
771
+ "required": False,
772
+ "description": "Limits the number of features returned in the response.",
773
+ "schema": {
774
+ "type": "integer",
775
+ "minimum": 1,
776
+ "maximum": profile.paging.max_limit,
777
+ "default": profile.paging.default_limit,
778
+ },
779
+ }
780
+
781
+
782
+ def _with_paging_limit(params: list[dict], profile: "ServiceProfile | None") -> list[dict]:
783
+ """Append the paging `limit` param to a copy of *params* if enabled and absent."""
784
+ limit_param = _paging_limit_param(profile)
785
+ if limit_param is None:
786
+ return params
787
+ out = list(params)
788
+ if not any(isinstance(p, dict) and p.get("name") == "limit" for p in out):
789
+ out.append(limit_param)
790
+ return out
791
+
792
+
764
793
  def _collection_paths(coll: Collection, examples: dict | None = None,
765
794
  profile: ServiceProfile | None = None) -> dict:
766
795
  paths: dict = {}
@@ -793,12 +822,14 @@ def _collection_paths(coll: Collection, examples: dict | None = None,
793
822
  }}
794
823
 
795
824
  # Always generate Features paths (items, queryables, schema)
825
+ items_params = _with_paging_limit([_F, _LANG], profile)
826
+
796
827
  paths[f"{base}/items"] = {"get": {
797
828
  "summary": f"Get {coll.title or coll.id} items",
798
829
  "description": desc,
799
830
  "operationId": _operation_id("get", coll.id, "Features"),
800
831
  "tags": [tag],
801
- "parameters": [_F, _LANG],
832
+ "parameters": items_params,
802
833
  "responses": {
803
834
  "200": _R200_FEATURES,
804
835
  "400": _ERR_400, "404": _ERR_404, "500": _ERR_500,
@@ -863,6 +894,8 @@ def _collection_paths(coll: Collection, examples: dict | None = None,
863
894
  # instance-level query sub-paths — GET + optional POST
864
895
  for sub_qt in (active - {"instances"}):
865
896
  sub_params = _params_with_default_f(_QUERY_PARAMS.get(sub_qt, []), profile, _vars(sub_qt))
897
+ if sub_qt == "items":
898
+ sub_params = _with_paging_limit(sub_params, profile)
866
899
  get_op = {
867
900
  "summary": f"query {coll.id} instance by {sub_qt}",
868
901
  "description": desc,
@@ -877,12 +910,13 @@ def _collection_paths(coll: Collection, examples: dict | None = None,
877
910
  paths[f"{base}/instances/{{instanceId}}/{sub_qt}"] = path_ops
878
911
 
879
912
  elif qt == "items":
913
+ items_params = _with_paging_limit(params, profile)
880
914
  get_op = {
881
915
  "summary": f"query {coll.id} by items",
882
916
  "description": desc,
883
917
  "operationId": _operation_id("queryItems", coll.id),
884
918
  "tags": [tag],
885
- "parameters": params,
919
+ "parameters": items_params,
886
920
  "responses": cov_resp,
887
921
  }
888
922
  path_ops = {"get": get_op}
@@ -1376,6 +1410,135 @@ def _individual_test_adoc(profile: ServiceProfile, test_id: str) -> str:
1376
1410
 
1377
1411
  _PDF_OVERRIDE_FILENAME = "pdf_override.xsl"
1378
1412
 
1413
+ _OVERRIDE_SUPPRESS_SECTION_DIVIDERS = """
1414
+ <!-- Suppress section-divider pages by rendering only the main content flow. -->
1415
+ <xsl:template match="node()" mode="sections">
1416
+ <xsl:param name="num"/>
1417
+ <xsl:param name="initial-page-number"/>
1418
+ <fo:page-sequence xsl:use-attribute-sets="page-sequence-main">
1419
+ <xsl:call-template name="refine_page-sequence-main"/>
1420
+ <xsl:call-template name="insertFootnoteSeparator"/>
1421
+ <xsl:call-template name="insertHeaderFooter">
1422
+ <xsl:with-param name="num" select="$num"/>
1423
+ </xsl:call-template>
1424
+ <fo:flow flow-name="xsl-region-body">
1425
+ <fo:block line-height="125%">
1426
+ <xsl:choose>
1427
+ <xsl:when test=".//mn:indexsect">
1428
+ <xsl:apply-templates select=".//mn:indexsect" mode="index"/>
1429
+ </xsl:when>
1430
+ <xsl:otherwise>
1431
+ <xsl:apply-templates/>
1432
+ </xsl:otherwise>
1433
+ </xsl:choose>
1434
+ </fo:block>
1435
+ </fo:flow>
1436
+ </fo:page-sequence>
1437
+ </xsl:template>
1438
+ """
1439
+
1440
+ _OVERRIDE_FLATTEN_BIBLIOGRAPHY = """
1441
+ <!-- Remove the hanging indent on Normative References: the OGC flavor uses
1442
+ start-indent 25mm / text-indent -25mm so continuation lines (the title)
1443
+ sit indented under the authors. Flatten both to 0 so every line aligns at
1444
+ the left margin. -->
1445
+ <xsl:attribute-set name="bibitem-normative-style">
1446
+ <xsl:attribute name="margin-bottom">12pt</xsl:attribute>
1447
+ <xsl:attribute name="start-indent">0mm</xsl:attribute>
1448
+ <xsl:attribute name="text-indent">0mm</xsl:attribute>
1449
+ <xsl:attribute name="line-height">115%</xsl:attribute>
1450
+ </xsl:attribute-set>
1451
+
1452
+ <!-- Same for the non-normative Bibliography section: render the citation tag
1453
+ inline with the title as one flush block (wrapped lines align at the left
1454
+ margin instead of being indented under the authors). -->
1455
+ <xsl:template match="mn:references[not(@normative='true')]/mn:bibitem" name="bibitem_non_normative" priority="2">
1456
+ <xsl:param name="skip"/>
1457
+ <xsl:call-template name="setNamedDestination"/>
1458
+ <fo:block id="{@id}" margin-bottom="12pt" role="SKIP">
1459
+ <fo:inline role="SKIP">
1460
+ <xsl:apply-templates select="mn:biblio-tag">
1461
+ <xsl:with-param name="biblio_tag_part">first</xsl:with-param>
1462
+ </xsl:apply-templates>
1463
+ </fo:inline>
1464
+ <xsl:text> </xsl:text>
1465
+ <xsl:call-template name="processBibitem">
1466
+ <xsl:with-param name="biblio_tag_part">last</xsl:with-param>
1467
+ </xsl:call-template>
1468
+ </fo:block>
1469
+ </xsl:template>
1470
+ """
1471
+
1472
+ _OVERRIDE_INLINE_SECTION_HEADERS = """
1473
+ <!-- Inline Level 1 section titles (e.g. '1. Scope' in a single block, no circles, no table). -->
1474
+ <xsl:template match="mn:fmt-title" name="title">
1475
+ <xsl:variable name="level"><xsl:call-template name="getLevel"/></xsl:variable>
1476
+ <xsl:variable name="element-name">
1477
+ <xsl:choose>
1478
+ <xsl:when test="../@inline-header = 'true'">fo:inline</xsl:when>
1479
+ <xsl:otherwise>fo:block</xsl:otherwise>
1480
+ </xsl:choose>
1481
+ </xsl:variable>
1482
+ <xsl:call-template name="setNamedDestination"/>
1483
+ <xsl:variable name="title_styles">
1484
+ <styles xsl:use-attribute-sets="title-style">
1485
+ <xsl:call-template name="refine_title-style"/>
1486
+ </styles>
1487
+ </xsl:variable>
1488
+
1489
+ <xsl:choose>
1490
+ <xsl:when test="$level = 1">
1491
+ <fo:block>
1492
+ <xsl:copy-of select="xalan:nodeset($title_styles)/styles/@*[local-name() = 'space-before' or local-name() = 'margin-bottom' or local-name() = 'keep-with-next' or local-name() = 'role']"/>
1493
+ <xsl:call-template name="setIDforNamedDestinationInline"/>
1494
+ <xsl:variable name="title">
1495
+ <xsl:choose>
1496
+ <xsl:when test="mn:tab">
1497
+ <xsl:copy-of select="mn:tab[1]/following-sibling::node()"/>
1498
+ </xsl:when>
1499
+ <xsl:otherwise><xsl:copy-of select="."/></xsl:otherwise>
1500
+ </xsl:choose>
1501
+ </xsl:variable>
1502
+ <xsl:variable name="section" select="mn:tab[1]/preceding-sibling::node()"/>
1503
+ <xsl:call-template name="insertSectionTitle">
1504
+ <xsl:with-param name="section" select="$section"/>
1505
+ <xsl:with-param name="title" select="$title"/>
1506
+ <xsl:with-param name="level" select="$level"/>
1507
+ </xsl:call-template>
1508
+ </fo:block>
1509
+ </xsl:when>
1510
+ <xsl:when test="$level = 2">
1511
+ <fo:block>
1512
+ <xsl:copy-of select="xalan:nodeset($title_styles)/styles/@*[local-name() = 'space-before' or local-name() = 'margin-bottom' or local-name() = 'keep-with-next' or local-name() = 'role']"/>
1513
+ <xsl:call-template name="setIDforNamedDestinationInline"/>
1514
+ <xsl:variable name="title">
1515
+ <xsl:choose>
1516
+ <xsl:when test="mn:tab">
1517
+ <xsl:copy-of select="mn:tab[1]/following-sibling::node()"/>
1518
+ </xsl:when>
1519
+ <xsl:otherwise><xsl:copy-of select="."/></xsl:otherwise>
1520
+ </xsl:choose>
1521
+ </xsl:variable>
1522
+ <xsl:variable name="section" select="mn:tab[1]/preceding-sibling::node()"/>
1523
+ <xsl:call-template name="insertSectionTitle">
1524
+ <xsl:with-param name="section" select="$section"/>
1525
+ <xsl:with-param name="title" select="$title"/>
1526
+ <xsl:with-param name="level" select="$level"/>
1527
+ </xsl:call-template>
1528
+ </fo:block>
1529
+ </xsl:when>
1530
+ <xsl:otherwise>
1531
+ <xsl:element name="{$element-name}">
1532
+ <xsl:copy-of select="xalan:nodeset($title_styles)/styles/@*"/>
1533
+ <xsl:call-template name="setIDforNamedDestinationInline"/>
1534
+ <xsl:apply-templates/>
1535
+ <xsl:apply-templates select="following-sibling::*[1][self::mn:variant-title][@type = 'sub']" mode="subtitle"/>
1536
+ </xsl:element>
1537
+ </xsl:otherwise>
1538
+ </xsl:choose>
1539
+ </xsl:template>
1540
+ """
1541
+
1379
1542
  # Individual template overrides for the OGC Metanorma flavor's PDF XSL. Each is
1380
1543
  # keyed to a real template name in metanorma-ogc's ogc.standard.xsl and merged
1381
1544
  # in via :pdf-stylesheet-override:. These are coupled to the OGC flavor.
@@ -1519,10 +1682,19 @@ def _effective_pdf_config(m) -> dict:
1519
1682
  (default white) background with dark, readable numbers/titles so they no
1520
1683
  longer carry the OGC navy house style.
1521
1684
  """
1685
+ # suppress_design_elements ("debrand") controls only the OGC *decorative*
1686
+ # motifs — the blue crossing lines, the circular clause-number badges and the
1687
+ # short rule under titles — plus the section-divider page recolouring. It does
1688
+ # NOT remove the section-divider pages, inline the headers, or reflow the
1689
+ # bibliography: those are structural/layout choices left as independent opt-ins
1690
+ # so the default OGC document keeps its divider pages and standard layout.
1522
1691
  debrand = bool(getattr(m, "suppress_design_elements", False))
1523
1692
  crossing = debrand or bool(getattr(m, "suppress_crossing_lines", False))
1524
1693
  underlines = debrand or bool(getattr(m, "suppress_title_underlines", False))
1525
1694
  plain_nums = debrand or bool(getattr(m, "plain_section_numbers", False))
1695
+ suppress_divs = bool(getattr(m, "suppress_section_divider_pages", False))
1696
+ inline_headers = bool(getattr(m, "inline_section_headers", False))
1697
+ biblio_indent = bool(getattr(m, "suppress_bibliography_indent", False))
1526
1698
 
1527
1699
  colors = getattr(m, "colors", None)
1528
1700
  page_bg = getattr(colors, "page_background", None) if colors else None
@@ -1548,6 +1720,9 @@ def _effective_pdf_config(m) -> dict:
1548
1720
  "crossing": crossing,
1549
1721
  "underlines": underlines,
1550
1722
  "plain_nums": plain_nums,
1723
+ "suppress_divs": suppress_divs,
1724
+ "inline_headers": inline_headers,
1725
+ "biblio_indent": biblio_indent,
1551
1726
  "body_font": getattr(m, "body_font", None),
1552
1727
  "watermark": getattr(m, "page_watermark", None),
1553
1728
  "divider_bg": divider_bg,
@@ -1555,6 +1730,32 @@ def _effective_pdf_config(m) -> dict:
1555
1730
  }
1556
1731
 
1557
1732
 
1733
+ def _submitting_orgs_text_override(org_name: str) -> str:
1734
+ """Rebrand the OGC flavor's auto-generated 'Submitting Organizations' sentence.
1735
+
1736
+ The OGC Metanorma flavor hardcodes "...submitted this Document to the Open
1737
+ Geospatial Consortium (OGC):" in its presentation stage (not the i18n YAML),
1738
+ so it lands in the presentation XML as the text of the
1739
+ ``clause[@type='submitting_orgs']`` paragraph. We replace just that text node
1740
+ with the rebranded SDO name, leaving the paragraph's styling intact.
1741
+
1742
+ Fragile by nature: coupled to the exact sentence the OGC flavor emits. If the
1743
+ flavor changes that wording, this literal will no longer match and should be
1744
+ updated.
1745
+ """
1746
+ org = _xml_escape(org_name)
1747
+ return f"""
1748
+ <!-- Rebrand the flavor's auto-generated 'Submitting Organizations' sentence
1749
+ (the OGC flavor injects 'the Open Geospatial Consortium (OGC)' at its
1750
+ presentation stage). Match the clause's paragraph element specifically —
1751
+ more specific than the base mn:p template, so mn2pdf's merge appends this
1752
+ and it wins for that paragraph only, leaving all other paragraphs alone. -->
1753
+ <xsl:template match="mn:clause[@type='submitting_orgs']/mn:p">
1754
+ <fo:block xsl:use-attribute-sets="p-style" role="P">The following organizations submitted this Document to {org}:</fo:block>
1755
+ </xsl:template>
1756
+ """
1757
+
1758
+
1558
1759
  def _pdf_override_flags(m) -> list[str]:
1559
1760
  """Return the override snippets enabled by document_metadata, in order."""
1560
1761
  if not m:
@@ -1569,6 +1770,17 @@ def _pdf_override_flags(m) -> list[str]:
1569
1770
  parts.append(_OVERRIDE_SUPPRESS_TITLE_UNDERLINES)
1570
1771
  if cfg["plain_nums"]:
1571
1772
  parts.append(_plain_section_numbers_override(cfg["divider_fg"]))
1773
+ if cfg["suppress_divs"]:
1774
+ parts.append(_OVERRIDE_SUPPRESS_SECTION_DIVIDERS)
1775
+ if cfg["inline_headers"]:
1776
+ parts.append(_OVERRIDE_INLINE_SECTION_HEADERS)
1777
+ if cfg["biblio_indent"]:
1778
+ parts.append(_OVERRIDE_FLATTEN_BIBLIOGRAPHY)
1779
+ # Rebrand the flavor's auto-generated "Submitting Organizations" sentence to
1780
+ # name the copyright holder (e.g. DGIWG) instead of OGC, when rebranding.
1781
+ holder = getattr(m, "copyright_holder", None)
1782
+ if holder and holder.strip().upper() not in ("OGC", "OPEN GEOSPATIAL CONSORTIUM"):
1783
+ parts.append(_submitting_orgs_text_override(holder))
1572
1784
  # When the divider background is rebranded (light), force the big title
1573
1785
  # colour too — the flavor inherits white text, invisible on a light page.
1574
1786
  if cfg["divider_bg"] is not None:
@@ -1699,7 +1911,7 @@ def _build_document_adoc(profile: ServiceProfile) -> str:
1699
1911
  ":committee: technical",
1700
1912
  f":docnumber: {m.doc_number if m else profile.name}",
1701
1913
  f":copyright-year: {year}",
1702
- f":published-date: {(m.publication_date if m and m.publication_date else default_date)}",
1914
+ f":published-date: {((m.doc_pub_date or m.publication_date) if m and (m.doc_pub_date or m.publication_date) else default_date)}",
1703
1915
  f":issued-date: {(m.approval_date if m and m.approval_date else default_date)}",
1704
1916
  f":received-date: {(m.submission_date if m and m.submission_date else default_date)}",
1705
1917
  ]
@@ -472,6 +472,11 @@ class CoverPage(BaseModel):
472
472
  "Note: this marks the cover only; an every-page watermark requires the Metanorma layer."
473
473
  ),
474
474
  )
475
+ logo_width: int = Field(default=420, description="Target logo width on cover page")
476
+ logo_y: int = Field(default=180, description="Logo y-offset from top of cover page")
477
+ tagline_font_size: int = Field(default=30, description="Font size of cover page tagline")
478
+ title_font_size: int = Field(default=54, description="Font size of cover page title")
479
+ bold_edition: bool = Field(default=False, description="When true, render edition in bold font")
475
480
 
476
481
  @field_validator("background", "text_color")
477
482
  @classmethod
@@ -599,6 +604,18 @@ class DocumentMetadata(BaseModel):
599
604
  "the flavor's built-in text via :boilerplate-authority:."
600
605
  ),
601
606
  )
607
+ doc_pub_date: str | None = Field(default=None, description="Alternative field for publication date (YYYY-MM-DD)")
608
+ suppress_section_divider_pages: bool = Field(default=False, description="When true, completely remove section-divider pages from the PDF")
609
+ inline_section_headers: bool = Field(default=False, description="When true, render Level 1 headings inline without circles or table formatting")
610
+ suppress_bibliography_indent: bool = Field(
611
+ default=False,
612
+ description=(
613
+ "When true, remove the OGC flavor's hanging indent on non-normative "
614
+ "bibliography/reference entries so the citation tag and title render as a "
615
+ "single flush paragraph (wrapped lines align at the left margin instead of "
616
+ "being indented under the authors). Coupled to the OGC Metanorma flavor."
617
+ ),
618
+ )
602
619
  suppress_flavor_logo: bool = Field(
603
620
  default=False,
604
621
  description=(
@@ -651,10 +668,15 @@ class DocumentMetadata(BaseModel):
651
668
  default=False,
652
669
  description=(
653
670
  "When true, strip the OGC flavor's decorative PDF design elements — the "
654
- "blue 'crossing lines' motif, the circular badges around clause numbers, and "
655
- "the short rule under clause titles — via a targeted PDF stylesheet override. "
656
- "Produces a plainer 'i. Abstract' / '1 Scope' heading style for SDOs (e.g. "
657
- "DGIWG) that don't use the OGC house style. Coupled to the OGC Metanorma flavor."
671
+ "blue 'crossing lines' motif, the circular badges around clause numbers, "
672
+ "and the short rule under clause titles — via a targeted PDF stylesheet "
673
+ "override, and recolour the section-divider pages to a clean light "
674
+ "background for SDOs (e.g. DGIWG) that don't use the OGC house style. "
675
+ "This affects only decorative styling: it does NOT remove the "
676
+ "section-divider pages, inline the section headers, or reflow the "
677
+ "bibliography — use suppress_section_divider_pages, inline_section_headers "
678
+ "and suppress_bibliography_indent for those structural changes. Coupled to "
679
+ "the OGC Metanorma flavor."
658
680
  ),
659
681
  )
660
682
  page_watermark: str | None = Field(
@@ -666,7 +688,7 @@ class DocumentMetadata(BaseModel):
666
688
  ),
667
689
  )
668
690
 
669
- @field_validator("submission_date", "approval_date", "publication_date")
691
+ @field_validator("submission_date", "approval_date", "publication_date", "doc_pub_date")
670
692
  @classmethod
671
693
  def _valid_date(cls, v: str | None) -> str | None:
672
694
  if v is not None and not _DATE_RE.match(v):
@@ -700,6 +722,13 @@ class PubSubConfig(BaseModel):
700
722
  collection_filters: dict[str, CollectionPubSub] = Field(default_factory=dict, description="Per-collection filter overrides")
701
723
 
702
724
 
725
+ class PagingConfig(BaseModel):
726
+ """Optional pagination parameters config."""
727
+ enabled: bool = Field(default=True, description="When true, include standard limit parameter in /items endpoint")
728
+ default_limit: int = Field(default=10, description="Default number of features to return", ge=1)
729
+ max_limit: int = Field(default=10000, description="Maximum allowed limit for features returned", ge=1)
730
+
731
+
703
732
  # ---------------------------------------------------------------------------
704
733
  # Root model — the authoritative profile definition
705
734
  # ---------------------------------------------------------------------------
@@ -767,6 +796,7 @@ class ServiceProfile(BaseModel):
767
796
  pubsub: PubSubConfig | None = None
768
797
  processes: list[dict] = Field(default_factory=list)
769
798
  document_metadata: DocumentMetadata | None = None
799
+ paging: PagingConfig = Field(default_factory=PagingConfig)
770
800
 
771
801
  # OGC API - EDR Part 3 specific fields
772
802
  required_conformance_classes: list[str] = Field(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oapi-profile-builder
3
- Version: 3.3.0
3
+ Version: 3.4.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