diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..d786845
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,10 @@
+
+Notable changes and version history.
+
+| Version | Date | Comment |
+|---------|-------|-------|
+| [v0.3.1](https://github.com/network-wrangler/projectcard/releases/tag/v0.3.1) | 2024-10-07 | Improved resilience of `read_cards()` including ability to handle relative paths. |
+| [v0.3.0](https://github.com/network-wrangler/projectcard/releases/tag/v0.3.0) | 2024-09-27 | Added transit addition and deletion change types. |
+| [v0.2.0](https://github.com/network-wrangler/projectcard/releases/tag/v0.2.0) | 2024-09-08 | Read cards from nested folders. |
+| [v0.1.2](https://github.com/network-wrangler/projectcard/releases/tag/v0.1.2) | 2024-08-05 | - |
+| [v0.1.1](https://github.com/network-wrangler/projectcard/releases/tag/v0.1.1) | 2024-06-20 | Initial release on PyPI |
diff --git a/docs/development.md b/docs/development.md
index f01eb87..01a5dbe 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -133,3 +133,10 @@ mkdocs serve
!!! tip
Releases must have a unique version number in order to be updated on pypi.
+
+## Changelog
+
+{!
+ include-markdown "../CHANGELOG.md"
+ heading-offset=1
+!}
diff --git a/docs/index.md b/docs/index.md
index a0df2ee..2e78f3b 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -84,4 +84,11 @@ But we'd love to make it better! Please report bugs or incorrect/unclear/missing
## Who-dat?
-ProjectCard was developed using resources from the [Metropolitan Transportation Commission](bayareametro.gov), [Metropolitan Council MN](https://metrocouncil.org/), and in-kind time from [UrbanLabs LLC](https://urbanlabs.io) and [WSP](www.wsp.com). It is currently maintained using in-kind time...so please be patient.
+ProjectCard was developed using resources from the [Metropolitan Transportation Commission](https://www.bayareametro.gov), [Metropolitan Council MN](https://metrocouncil.org/), and in-kind time from [UrbanLabs LLC](https://urbanlabs.io) and [WSP](https://www.wsp.com). It is currently maintained using in-kind time...so please be patient.
+
+## Release History
+
+{!
+ include-markdown "../CHANGELOG.md"
+ heading-offset=1
+!}
diff --git a/docs/json-schemas.md b/docs/json-schemas.md
index 526d76a..288be1f 100644
--- a/docs/json-schemas.md
+++ b/docs/json-schemas.md
@@ -3,4 +3,4 @@
!!! tip "Find this documentation confusing?"
Me too. That's why we spent a bunch more time documenting the [data models](datamodels.md), which read much more nicely - or you can check out the [examples](examples.md).
-{{ document_schema(schema_filename="projectcard.json") }}
+{{ document_schema() }}
diff --git a/docs/requirements.txt b/docs/requirements.txt
index db996d8..76358de 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,6 +1,5 @@
fontawesome_markdown
-json_schema_for_humans
-jsonschema2md
+jsonschema-markdown
mike
mkdocs
mkdocs-autorefs
diff --git a/main.py b/main.py
index 3b75bab..e3516b8 100644
--- a/main.py
+++ b/main.py
@@ -15,42 +15,11 @@ def define_env(env):
"""
@env.macro
- def document_schema(schema_filename: str) -> str:
- from json_schema_for_humans.generate import generate_from_schema
- from json_schema_for_humans.generation_configuration import (
- GenerationConfiguration,
- )
-
- _rel_schema_path = SCHEMA_DIR / schema_filename
- _abs_schema_path = _rel_schema_path.absolute()
- if not _abs_schema_path.exists():
- msg = f"Schema doesn't exist at: {_abs_schema_path}"
- raise FileNotFoundError(msg)
-
- _config = GenerationConfiguration(
- minify=False,
- copy_css=False,
- copy_js=False,
- template_name="js",
- expand_buttons=True,
- )
-
- content = generate_from_schema(_abs_schema_path, config=_config)
-
- # get content ready for mkdocs
- _footer = _get_html_between_tags(content, tag="footer")
- replace_strings = {
- "": "",
- '
\
- Type: object
': "",
- }
-
- for _orig, _new in replace_strings.items():
- content = content.replace(_orig, _new)
-
- content = _get_html_between_tags(content, tag="body")
- content = _rm_html_between_tags(content, tag="footer")
- return content
+ def document_schema(**kwargs) -> str:
+ """Generate Markdown documentation for a JSON schema."""
+ from projectcard.docs import json_schema_to_md
+
+ return json_schema_to_md(**kwargs)
@env.macro
def list_examples(data_dir: Path) -> str:
diff --git a/mkdocs.yml b/mkdocs.yml
index e73f5aa..bb7514a 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -15,8 +15,12 @@ theme:
features:
- content.code.annotate
- content.code.copy
+ - content.tabs.link
+ - navigation.indexes
- navigation.tabs
+ - navigation.expand
- toc.integrate
+ - toc.follow
palette:
- scheme: default
primary: blue grey
@@ -27,10 +31,12 @@ theme:
plugins:
- autorefs
- awesome-pages
- - include-markdown
- - mike
+ - include-markdown:
+ opening_tag: "{!"
+ closing_tag: "!}"
- macros
- mermaid2
+ - mike
- mkdocs-jupyter:
include_source: True
- mkdocstrings:
@@ -52,12 +58,10 @@ extra:
default: latest
extra_javascript:
- - https://code.jquery.com/jquery-3.4.1.min.js
- - https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js
+ - https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js
- javascript/schema_doc.min.js
extra_css:
-
- extra_css/extra.css
markdown_extensions:
@@ -65,11 +69,13 @@ markdown_extensions:
- codehilite:
linenums: true
- meta
- - pymdownx.inlinehilite
+ - pymdownx.details
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
+ use_pygments: true
+ - pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.tasklist:
custom_checkbox: true
diff --git a/projectcard/__init__.py b/projectcard/__init__.py
index e3dd3ce..774f57e 100644
--- a/projectcard/__init__.py
+++ b/projectcard/__init__.py
@@ -1,8 +1,9 @@
"""Project Card representation and validation."""
+from .errors import ProjectCardReadError, PycodeError, ValidationError
from .io import read_card, read_cards, write_card
from .logger import CardLogger, setup_logging
from .projectcard import ProjectCard, SubProject
-from .validate import PycodeError, ValidationError, validate_card, validate_schema_file
+from .validate import validate_card, validate_schema_file
__version__ = "0.3.0"
diff --git a/projectcard/docs.py b/projectcard/docs.py
index 780563f..db8f3ab 100644
--- a/projectcard/docs.py
+++ b/projectcard/docs.py
@@ -1,12 +1,17 @@
"""Utilities to assist in documentation which may be useful for other purposes."""
+
from __future__ import annotations
+import json
+from collections import defaultdict
from pathlib import Path
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Literal, Union
-from .utils import slug_to_str, make_slug
-from .io import read_cards
+import pandas as pd
+from .io import read_cards
+from .utils import make_slug, slug_to_str
+from .validate import _open_json
if TYPE_CHECKING:
from .projectcard import ProjectCard
@@ -22,7 +27,7 @@ def card_to_md(card: ProjectCard) -> str:
return _card_md
-def _categories_as_str(change_types: list[str] ) -> str:
+def _categories_as_str(change_types: list[str]) -> str:
if len(change_types) == 1:
return slug_to_str(change_types[0])
@@ -32,15 +37,12 @@ def _categories_as_str(change_types: list[str] ) -> str:
def _card_to_mdrow(card):
- _md_row = (
- f"| [{card.project.title()}](#{make_slug(card.project).replace('_','-')}) | "
- )
+ _md_row = f"| [{card.project.title()}](#{make_slug(card.project).replace('_','-')}) | "
_md_row += f" {_categories_as_str(card.change_types)} |"
_md_row += f" {card.notes} |\n"
return _md_row
-
def card_list_to_table(card_dir: Path) -> str:
"""Generates a table of all project cards in a directory followed by the cards."""
CARD_LIST_TABLE_FIELDS = ["Category", "Notes"]
@@ -59,4 +61,307 @@ def card_list_to_table(card_dir: Path) -> str:
md_table += _card_to_mdrow(card)
md_examples += card_to_md(card)
- return md_table + md_examples
\ No newline at end of file
+ return md_table + md_examples
+
+
+ROOTDIR = Path(__file__).resolve().parent
+PROJECTCARD_SCHEMA = ROOTDIR / "schema" / "projectcard.json"
+
+
+def json_schema_to_md(schema_path: Path = PROJECTCARD_SCHEMA) -> str:
+ """Generate Markdown documentation for a JSON schema."""
+ if not schema_path.is_absolute():
+ rel_schema_path = ROOTDIR / schema_path
+ schema_path = rel_schema_path.absolute()
+ schema_dir = schema_path.parent
+ schema_dict = _get_dict_of_schemas(schema_dir)
+ sorted_folders = sorted(schema_dict.keys())
+ markdown_output = f"{schema_path.root}\n"
+ for rel_folder in sorted_folders:
+ for schema_file in schema_dict[rel_folder]:
+ markdown_output += _single_jsonschema_to_md(schema_file, rel_folder)
+
+ return markdown_output
+
+
+def _list_to_bullets(items: list, fmt: Literal["html", "md"] = "md") -> str:
+ """Convert a list of items to a bulleted list in markdown or html."""
+ if fmt == "html":
+ return "" + "".join(f"- {json.dumps(item)}
" for item in items) + "\n
"
+ if fmt == "md":
+ return "\n".join(f"- `{json.dumps(item)}`" for item in items)
+ msg = "fmt must be 'html' or 'md'"
+ raise ValueError(msg)
+
+
+def _restrictions_to_md(item: dict, list_fmt: Literal["html", "md"] = "md") -> str:
+ required_md = ""
+ lb = "" if list_fmt == "html" else "\n\n"
+ if "required" in item:
+ required_md += (
+ f"**Required:**{lb}"
+ + _fmt_array_or_obj_or_str(item["required"], list_fmt=list_fmt)
+ + f"{lb}"
+ )
+ if "anyOf" in item:
+ required_md += (
+ f"**Any Of:**{lb}"
+ + _fmt_array_or_obj_or_str(item["anyOf"], list_fmt=list_fmt)
+ + f"{lb}"
+ )
+ if "oneOf" in item:
+ required_md += (
+ f"**One Of:**{lb}"
+ + _fmt_array_or_obj_or_str(item["oneOf"], list_fmt=list_fmt)
+ + f"{lb}"
+ )
+ if "enum" in item:
+ required_md += "**Enumeration:** `" + "`,`".join(list(map(str, item["enum"]))) + f"`{lb}"
+ return required_md
+
+
+def _properties_to_md(properties: dict) -> str:
+ """Convert a dictionary of properties to a markdown table."""
+ rows = []
+ for prop, details in properties.items():
+ prop_type = _get_type_txt(details)
+ # Handle anyOf and oneOf
+ row = {
+ "Property": f"`{prop}`",
+ "Type": prop_type,
+ "Description": details.get(
+ "description", details.get("title", details.get("name", "-"))
+ ),
+ "Restrictions": _restrictions_to_md(details),
+ }
+ rows.append(row)
+ properties_df = pd.DataFrame(rows)
+ properties_md = properties_df.to_markdown(index=False)
+ return properties_md
+
+
+def _defs_to_md(defs: dict) -> str:
+ """Convert a dictionary of $defs to a markdown table with anchors."""
+ rows = []
+ defs_md = ""
+ for def_name, details in defs.items():
+ row = {
+ "Definition": f"`{def_name}`",
+ "Type": _get_type_txt(details),
+ "Description": details.get("description", details.get("name", "-")),
+ "Restrictions": _restrictions_to_md(details, list_fmt="html"),
+ }
+ rows.append(row)
+ # anchor = def_name.replace('/', '_')
+ # defs_md += f'\n\n'
+ defs_df = pd.DataFrame(rows)
+ defs_md += defs_df.to_markdown(index=False)
+ return defs_md
+
+
+def _examples_to_md(examples: list, schema_name: str) -> str:
+ """Convert a list of examples to markdown."""
+ examples_md = ""
+ indent = " "
+ if examples:
+ for i, example in enumerate(examples, 1):
+ examples_md += f'??? example "{schema_name} Example {i}"\n'
+ examples_md += f"{indent}```json\n"
+ examples_md += indent + json.dumps(example, indent=4).replace("\n", f"\n{indent}")
+ examples_md += f"\n{indent}```\n"
+ return examples_md
+
+
+def _raw_schema_to_md(schema: dict, schema_name: str) -> str:
+ """Convert a raw JSON schema to drop-down admonition."""
+ raw_md = ""
+ indent = " "
+ raw_md += f'??? abstract "{schema_name} Contents"\n'
+ raw_md += f"{indent}```json\n"
+ raw_md += indent + json.dumps(schema, indent=4).replace("\n", f"\n{indent}")
+ raw_md += f"\n{indent}```\n"
+ return raw_md
+
+
+def _object_to_md(object_schema: dict, schema_name: str) -> str:
+ """Convert json-schema object information to markdown."""
+ defs = object_schema.get("$defs", {})
+ properties = object_schema.get("properties", {})
+ examples = object_schema.get("examples", [])
+ SKIP = ["properties", "required", "oneOf", "anyOf", "$defs", "$schema", "examples", "type"]
+ additional_info = {k: v for k, v in object_schema.items() if k not in SKIP}
+
+ # Generate $defs table
+ defs_md = ""
+ if defs:
+ defs_md = "\n**Definitions**:\n\n"
+ defs_md += _defs_to_md(defs) + "\n\n"
+
+ # Generate properties table
+ properties_md = "\n**Properties**:\n\n" + _properties_to_md(properties)
+
+ # Generate required fields list
+ required_md = _restrictions_to_md(object_schema, list_fmt="html")
+
+ # Generate examples
+ examples_md = _examples_to_md(examples, schema_name) + "\n\n" if examples else ""
+
+ # Generate additional information
+ additional_md = ""
+ if additional_info:
+ additional_md = "**Additional Information**:\n\n"
+ additional_md += _fmt_array_or_obj_or_str(additional_info)
+
+ # Combine all parts
+ object_md = f"\n*Type:* Object\n\n"
+ if additional_md:
+ object_md += additional_md + "\n\n"
+ if defs_md:
+ object_md += defs_md + "\n\n"
+ object_md += properties_md + "\n\n"
+ if required_md:
+ object_md += required_md + "\n\n"
+ if examples_md:
+ object_md += examples_md + "\n\n"
+
+ return object_md
+
+
+def _get_type_txt(item: dict) -> str:
+ if "items" in item:
+ t = item["items"].get("type", item.get("$ref", "Any"))
+ return f"`array` of `{t}` items."
+ if "$ref" in item:
+ return f"`{item['$ref']}`"
+ return f"`{item.get('type', 'Any')}`"
+
+
+def _fmt_array_or_obj_or_str(
+ item: Union[dict, list, str], list_fmt: Literal["html", "md"] = "md"
+) -> str:
+ lb = "" if list_fmt == "html" else "\n\n"
+ if isinstance(item, dict):
+ if len(item) == 0:
+ return ""
+ if len(item) == 1:
+ for k, v in item.items():
+ if isinstance(v, list):
+ return f"{k}:{lb}{_list_to_bullets(v, fmt=list_fmt)}"
+ return f"{k}: {v}"
+ else:
+ md = ""
+ for k, v in item.items():
+ if isinstance(v, list):
+ bullet_list = _list_to_bullets(v, fmt=list_fmt).replace("\n", "\n ")
+ md += f"{k}:{lb} {bullet_list}"
+ md += f"- {k}: {v}{lb}"
+ return md
+ elif isinstance(item, list):
+ return _list_to_bullets(item, fmt=list_fmt)
+ return str(item)
+
+
+def _array_to_md(array_item: dict, schema_name: str) -> str:
+ """Convert json-schema array information to markdown."""
+ if "type" not in array_item or array_item["type"] != "array":
+ return "Invalid array schema"
+
+ array_md = f"\n*Type:* {_get_type_txt(array_item)}\n\n"
+
+ SKIP = ["type", "$ref"]
+ restrictions = {
+ k: _fmt_array_or_obj_or_str(v)
+ for k, v in array_item.get("items", {}).items()
+ if k not in SKIP
+ }
+ if restrictions:
+ array_md += "| Property | Value |\n"
+ array_md += "|----------|-------|\n"
+ for key, value in restrictions.items():
+ array_md += f"| **{key}** | {value} |\n"
+ array_md += "\n"
+
+ # Handle $defs if present
+ defs = array_item.get("$defs", {})
+ if defs:
+ array_md += f"\n**Definitions**:\n\n"
+ array_md += _defs_to_md(defs) + "\n\n"
+
+ # Generate examples
+ examples = array_item.get("examples", [])
+ examples_md = _examples_to_md(examples, schema_name) + "\n\n" if examples else ""
+ if examples_md:
+ array_md += examples_md + "\n\n"
+
+ return array_md
+
+
+def _other_type_to_md(schema_data, schema_name):
+ content_md = f"Schema Type: {_get_type_txt(schema_data)}\n\n"
+
+ # Generate additional information
+ SKIP = [
+ "properties",
+ "required",
+ "oneOf",
+ "anyOf",
+ "$defs",
+ "$schema",
+ "examples",
+ "type",
+ "enum",
+ ]
+ additional_info = {k: v for k, v in schema_data.items() if k not in SKIP}
+ if additional_info:
+ content_md = "**Additional Information**:\n\n"
+ content_md += _fmt_array_or_obj_or_str(additional_info) + "\n\n"
+
+ restrictions_md = _restrictions_to_md(schema_data, list_fmt="html")
+ if restrictions_md:
+ content_md += restrictions_md + "\n\n"
+
+ # Handle $defs if present
+ defs = schema_data.get("$defs", {})
+ if defs:
+ content_md += f"\n**Definitions**:\n\n"
+ content_md += _defs_to_md(defs) + "\n\n"
+
+ # Generate examples
+ examples = schema_data.get("examples", [])
+ if examples:
+ examples_md = _examples_to_md(examples, schema_name) + "\n\n" if examples else ""
+ content_md += examples_md + "\n\n"
+ return content_md
+
+
+def _single_jsonschema_to_md(schema_file: Path, rel_folder: Path) -> str:
+ heading_level = len(rel_folder.parts) + 1 # +1 because base directory doesn't count
+ header = "\n##"
+ if heading_level > 1:
+ header += f"{'#' * heading_level} {'.'.join(rel_folder.parts)}."
+ header += f"{schema_file.name}\n"
+
+ schema_data = _open_json(schema_file)
+ schema_name = schema_data.get("title", f"`{schema_file.stem}`")
+
+ # Determine the type of schema and call the appropriate function
+ schema_type = schema_data.get("type", "unknown")
+ if schema_type == "array":
+ content_md = _array_to_md(schema_data, schema_name)
+ elif schema_type == "object":
+ content_md = _object_to_md(schema_data, schema_name)
+ else:
+ content_md = _other_type_to_md(schema_data, schema_name)
+
+ content_md += "\n" + _raw_schema_to_md(schema_data, schema_name) + "\n"
+
+ return header + content_md
+
+
+def _get_dict_of_schemas(base_dir: Path, extension: str = ".json") -> defaultdict:
+ base_dir = Path(base_dir)
+
+ files_by_folder = defaultdict(list)
+ for schema_file in base_dir.rglob(f"*{extension}"):
+ files_by_folder[schema_file.parent.relative_to(base_dir)].append(schema_file)
+ return files_by_folder
diff --git a/projectcard/errors.py b/projectcard/errors.py
new file mode 100644
index 0000000..6140efd
--- /dev/null
+++ b/projectcard/errors.py
@@ -0,0 +1,22 @@
+"""Custom errors for projectcard package."""
+from jsonschema.exceptions import SchemaError, ValidationError
+
+
+class ProjectCardReadError(Exception):
+ """Error in reading project card."""
+
+
+class ProjectCardValidationError(ValidationError):
+ """Error in formatting of ProjectCard."""
+
+
+class SubprojectValidationError(ProjectCardValidationError):
+ """Error in formatting of Subproject."""
+
+
+class PycodeError(ProjectCardValidationError):
+ """Basic runtime error in python code."""
+
+
+class ProjectCardJSONSchemaError(SchemaError):
+ """Error in the ProjectCard json schema."""
\ No newline at end of file
diff --git a/projectcard/io.py b/projectcard/io.py
index 2b60035..8a57c6e 100644
--- a/projectcard/io.py
+++ b/projectcard/io.py
@@ -9,6 +9,7 @@
import toml
import yaml
+from .errors import ProjectCardReadError
from .logger import CardLogger
from .projectcard import REPLACE_KEYS, VALID_EXT, ProjectCard
@@ -17,11 +18,6 @@
DEFAULT_BASE_PATH = Path.cwd()
-
-class ProjectCardReadError(Exception):
- """Error in reading project card."""
-
-
SKIP_READ = ["valid"]
SKIP_WRITE = ["valid"]
@@ -98,6 +94,7 @@ def _read_wrangler(filepath: Path) -> dict:
def write_card(project_card, filename: Optional[Path] = None):
"""Writes project card dictionary to YAML file."""
from .utils import make_slug
+
default_filename = make_slug(project_card.project) + ".yml"
filename = filename or Path(default_filename)
diff --git a/projectcard/models/changes.py b/projectcard/models/changes.py
index 5c6c6e2..9525f8e 100644
--- a/projectcard/models/changes.py
+++ b/projectcard/models/changes.py
@@ -18,7 +18,7 @@
class RoadwayDeletion(BaseModel):
"""Requirements for describing roadway deletion project card (e.g. to delete).
- Parameters:
+ Attributes:
links (Optional[SelectRoadLinks]): Roadway links to delete.
nodes (Optional[SelectRoadNodes]): Roadway nodes to delete.
clean_shapes (bool): If True, will clean unused roadway shapes associated with the deleted links
@@ -53,7 +53,7 @@ class RoadwayDeletion(BaseModel):
class RoadwayAddition(BaseModel):
"""Requirements for describing roadway addition project card.
- Parameters:
+ Attributes:
links (Optional[list[RoadLink]]): Roadway links to add. Must have at least one link.
nodes (Optional[list[RoadNode]]): Roadway nodes to add. Must have at least one node.
@@ -93,7 +93,7 @@ class RoadwayAddition(BaseModel):
class RoadwayPropertyChanges(BaseModel):
"""Value for setting property changes for a time of day and category.
- Parameters:
+ Attributes:
facility (SelectFacility): Selection of roadway links to change properties for.
property_changes (dict[str, RoadwayPropertyChange]): Property changes to apply to the selection. Must have at least one property change.
@@ -131,7 +131,7 @@ class RoadwayPropertyChanges(BaseModel):
class TransitPropertyChange(BaseModel):
"""Value for setting property change for a time of day and category.
- Parameters:
+ Attributes:
service (SelectTransitTrips): Selection of transit trips to change properties for.
property_changes (dict[str, TransitPropertyChange]): List of property changes to apply.
@@ -161,7 +161,7 @@ class TransitPropertyChange(BaseModel):
class TransitRoutingChange(BaseModel):
"""Value for setting routing change for transit.
- Parameters:
+ Attributes:
service (SelectTransitTrips): Selection of transit trips to change routing for.
transit_routing_change (TransitRoutingChange): Existing and changed routing as denoted as a list of nodes with
nodes where the route doesn't stop noted as negative integers.
@@ -195,7 +195,7 @@ class TransitRoutingChange(BaseModel):
class TransitServiceDeletion(BaseModel):
"""Requirements for describing transit service deletion project card (e.g. to delete).
- Parameters:
+ Attributes:
service (SelectTransitTrips): Selection of transit trips to delete.
clean_shapes (Optional[bool]): If True, will clean unused transit shapes associated with the deleted trips
if they are not otherwise being used. Defaults to False.
@@ -225,7 +225,7 @@ class TransitServiceDeletion(BaseModel):
class TransitRouteAddition(BaseModel):
"""Requirements for describing transit route addition project card.
- Parameters:
+ Attributes:
routes (list[TransitRoute]): List of transit routes to be added. Must have at least one route.
!!! Example "Example Transit Route Addition"
diff --git a/projectcard/models/project.py b/projectcard/models/project.py
index 6d530d0..c8c3fdc 100644
--- a/projectcard/models/project.py
+++ b/projectcard/models/project.py
@@ -69,7 +69,7 @@ class ProjectModel(BaseModel):
List of tools that support json-schema:
- Parameters:
+ Attributes:
project (str): The name of the project. This name must be unique within a set of projects
being managed or applied to a network.
notes (Optional[str]): Additional freeform notes about the project.
diff --git a/projectcard/models/selections.py b/projectcard/models/selections.py
index f6985d9..35e49c6 100644
--- a/projectcard/models/selections.py
+++ b/projectcard/models/selections.py
@@ -17,7 +17,7 @@
class SelectRoadNodes(BaseModel):
"""Requirements for describing multiple nodes of a project card (e.g. to delete).
- Parameters:
+ Attributes:
all (bool): If True, select all nodes. Must have either `all`, `osm_node_id` or
`model_node_id`.
osm_node_id (Optional[list[str]]): List of OSM node IDs to select. Must have either
@@ -50,7 +50,7 @@ class SelectRoadLinks(BaseModel):
Additional fields to select on may be provided and will be treated as an AND condition after
the primary selection from `all`, `name`, `osm_link_id`, or `model_link_id`.
- Parameters:
+ Attributes:
all (bool): If True, select all links.
name (Optional[list[str]]): List of names to select. If multiple provided will
be treated as an OR condition.
@@ -126,7 +126,7 @@ class SelectTransitTrips(BaseModel):
Multiple requirements are treated as an AND condition.
- Parameters:
+ Attributes:
trip_properties (Optional[SelectTripProperties]): Selection based on trip properties.
route_properties (Optional[SelectRouteProperties]): Selection based on route properties.
timespans (List[Timespan]): List of timespans to select. Multiple timespans are treated
@@ -180,7 +180,7 @@ class SelectFacility(BaseModel):
continuous path - reulting in a final selection of links that may or may not connect
the two nodes.
- Parameters:
+ Attributes:
links (Optional[SelectRoadLinks]): Selection of roadway links.
nodes (Optional[SelectRoadNodes]): Selection of roadway nodes.
from (Optional[SelectRoadNode]): Selection of the origin node.
diff --git a/projectcard/projectcard.py b/projectcard/projectcard.py
index 0f9c4f4..7c66991 100644
--- a/projectcard/projectcard.py
+++ b/projectcard/projectcard.py
@@ -4,11 +4,10 @@
from typing import Union
+from .errors import ProjectCardValidationError, SubprojectValidationError
from .logger import CardLogger
from .utils import _findkeys
from .validate import (
- ProjectCardValidationError,
- SubprojectValidationError,
update_dict_with_schema_defaults,
validate_card,
)
diff --git a/projectcard/validate.py b/projectcard/validate.py
index a4868be..37df0ac 100644
--- a/projectcard/validate.py
+++ b/projectcard/validate.py
@@ -11,6 +11,7 @@
from jsonschema import validate
from jsonschema.exceptions import SchemaError, ValidationError
+from .errors import ProjectCardJSONSchemaError, ProjectCardValidationError, PycodeError
from .logger import CardLogger
ROOTDIR = Path(__file__).resolve().parent
@@ -24,22 +25,6 @@
FLAKE8_ERRORS = ["E9", "F821", "F823", "F405"]
-class ProjectCardValidationError(ValidationError):
- """Error in formatting of ProjectCard."""
-
-
-class SubprojectValidationError(ProjectCardValidationError):
- """Error in formatting of Subproject."""
-
-
-class PycodeError(ProjectCardValidationError):
- """Basic runtime error in python code."""
-
-
-class ProjectCardJSONSchemaError(SchemaError):
- """Error in the ProjectCard json schema."""
-
-
def _open_json(schema_path: Path) -> dict:
try:
with schema_path.open() as file:
@@ -59,6 +44,11 @@ def _load_schema(schema_absolute_path: Path) -> dict:
base_path = Path(schema_absolute_path).parent
base_uri = f"file:///{base_path}/"
+ if not schema_absolute_path.exists():
+ msg = f"Schema not found at {schema_absolute_path}"
+ CardLogger.error(msg)
+ raise FileNotFoundError(msg)
+
_s = jsonref.replace_refs(
_open_json(schema_absolute_path),
base_uri=base_uri,
diff --git a/pyproject.toml b/pyproject.toml
index 1f4758a..82b9bc3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,6 +30,7 @@ dependencies = [
"jsonschema",
"pydantic>=2.0",
"pyyaml",
+ "tabulate",
"toml",
]
diff --git a/tests/test_docs.py b/tests/test_docs.py
new file mode 100644
index 0000000..e88ffd4
--- /dev/null
+++ b/tests/test_docs.py
@@ -0,0 +1,42 @@
+"""Test Documentation builds and utilities."""
+
+import subprocess
+
+import markdown
+import pytest
+
+from projectcard.docs import card_list_to_table, json_schema_to_md
+from projectcard.logger import CardLogger
+
+
+def test_mkdocs_build(request):
+ """Tests that the MkDocs documentation can be built without errors."""
+ CardLogger.info(f"--Starting: {request.node.name}")
+ subprocess.run(["mkdocs", "build"], capture_output=True, text=True, check=True)
+ CardLogger.info(f"--Finished: {request.node.name}")
+
+
+def test_jsonschema2md(request, test_out_dir):
+ CardLogger.info(f"--Starting: {request.node.name}")
+ md = json_schema_to_md()
+ out_md = test_out_dir / "schema.md"
+ with out_md.open("w") as f:
+ f.write(md)
+ try:
+ markdown.markdown(md)
+ except Exception as e:
+ pytest.fail(f"json_schema_to_md generated Invalid markdown: {e}")
+ CardLogger.info(f"--Finished: {request.node.name}")
+
+
+def test_examples2md(request, example_dir, test_out_dir):
+ CardLogger.info(f"--Starting: {request.node.name}")
+ md = card_list_to_table(example_dir)
+ out_md = test_out_dir / "examples.md"
+ with out_md.open("w") as f:
+ f.write(md)
+ try:
+ markdown.markdown(md)
+ except Exception as e:
+ pytest.fail(f"card_list_to_table generated Invalid markdown: {e}")
+ CardLogger.info(f"--Finished: {request.node.name}")
\ No newline at end of file