diff --git a/.cookiecutter.json b/.cookiecutter.json
index 2e6fd32e..4981aea8 100644
--- a/.cookiecutter.json
+++ b/.cookiecutter.json
@@ -14,6 +14,17 @@
"project_with_config_settings": "no",
"generate_docs": "yes",
"version": "2.10.0",
- "original_publish_year": "2021"
+ "original_publish_year": "2021",
+ "_drift_manager": {
+ "template": "https://github.com/networktocode-llc/cookiecutter-ntc.git",
+ "template_dir": "python",
+ "template_ref": "main",
+ "cookie_dir": "",
+ "pull_request_strategy": "create",
+ "post_actions": [],
+ "draft": false,
+ "baked_commit_ref": "6e239e9b2ba0888f80cf1693318f007a2085d79a",
+ "drift_managed_branch": "develop"
+ }
}
}
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 585befea..b2100354 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,28 +1,24 @@
---
name: 🐛 Bug Report
-about: Report a reproducible bug in the current release of circuit_maintenance_parser
+about: Report a reproducible bug in the current release of circuit-maintenance-parser
---
### Environment
-
-- Python version:
-- circuit_maintenance_parser version:
+* Python version:
+* circuit-maintenance-parser version:
-
### Expected Behavior
-
+
### Observed Behavior
-
### Steps to Reproduce
-
1.
2.
3.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 82e36d60..f83b4c17 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,16 +1,15 @@
---
name: ✨ Feature Request
about: Propose a new feature or enhancement
+
---
### Environment
-
-- circuit_maintenance_parser version:
+* circuit-maintenance-parser version:
-
### Proposed Functionality
-
### Use Case
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 20edb902..4ad6c2c8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,15 +1,13 @@
---
name: "CI"
-concurrency: # Cancel any existing runs of this workflow for this same PR
- group: "${{ '{{ github.workflow }}' }}-${{ '{{ github.ref }}' }}"
+concurrency: # Cancel any existing runs of this workflow for this same PR
+ group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true
-on: # yamllint disable-line rule:truthy rule:comments
+on: # yamllint disable-line rule:truthy rule:comments
push:
branches:
- "main"
- "develop"
- tags:
- - "v*"
pull_request: ~
env:
@@ -18,7 +16,7 @@ env:
jobs:
ruff-format:
- runs-on: "ubuntu-24.04"
+ runs-on: "ubuntu-latest"
env:
INVOKE_PARSER_LOCAL: "True"
steps:
@@ -27,11 +25,11 @@ jobs:
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v6"
with:
- poetry-version: "1.8.5"
+ poetry-version: "2.1.3"
- name: "Linting: ruff format"
run: "poetry run invoke ruff --action format"
ruff-lint:
- runs-on: "ubuntu-24.04"
+ runs-on: "ubuntu-latest"
env:
INVOKE_PARSER_LOCAL: "True"
steps:
@@ -40,25 +38,25 @@ jobs:
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v6"
with:
- poetry-version: "1.8.5"
+ poetry-version: "2.1.3"
- name: "Linting: ruff"
run: "poetry run invoke ruff --action lint"
- # Temporarily disabled due to issues with the docs build and needing best practices for NTC python builds.
- # check-docs-build:
- # runs-on: "ubuntu-24.04"
- # env:
- # INVOKE_PARSER_LOCAL: "True"
- # steps:
- # - name: "Check out repository code"
- # uses: "actions/checkout@v4"
- # - name: "Setup environment"
- # uses: "networktocode/gh-action-setup-poetry-environment@v6"
- # with:
- # poetry-version: "1.8.5"
- # - name: "Check Docs Build"
- # run: "poetry run invoke build-and-check-docs"
+ check-docs-build:
+ runs-on: "ubuntu-latest"
+ env:
+ INVOKE_PARSER_LOCAL: "True"
+ steps:
+ - name: "Check out repository code"
+ uses: "actions/checkout@v4"
+ - name: "Setup environment"
+ uses: "networktocode/gh-action-setup-poetry-environment@v6"
+ with:
+ poetry-version: "2.1.3"
+ poetry-install-options: "--only dev,docs"
+ - name: "Check Docs Build"
+ run: "poetry run invoke build-and-check-docs"
poetry:
- runs-on: "ubuntu-24.04"
+ runs-on: "ubuntu-latest"
env:
INVOKE_PARSER_LOCAL: "True"
steps:
@@ -67,15 +65,11 @@ jobs:
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v6"
with:
- poetry-version: "1.8.5"
+ poetry-version: "2.1.3"
- name: "Checking: poetry lock file"
- run: "poetry lock --check"
- needs:
- - "ruff-format"
- - "ruff-lint"
- - "yamllint"
+ run: "poetry run invoke lock --check"
yamllint:
- runs-on: "ubuntu-24.04"
+ runs-on: "ubuntu-latest"
env:
INVOKE_PARSER_LOCAL: "True"
steps:
@@ -84,32 +78,36 @@ jobs:
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v6"
with:
- poetry-version: "1.8.5"
+ poetry-version: "2.1.3"
- name: "Linting: yamllint"
run: "poetry run invoke yamllint"
+ check-in-docker:
needs:
- "ruff-format"
- "ruff-lint"
- pylint:
- runs-on: "ubuntu-24.04"
+ - "poetry"
+ - "yamllint"
+ runs-on: "ubuntu-latest"
strategy:
fail-fast: true
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
env:
- PYTHON_VER: "${{ matrix.python-version }}"
+ INVOKE_PARSER_PYTHON_VER: "${{ matrix.python-version }}"
steps:
- name: "Check out repository code"
uses: "actions/checkout@v4"
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v6"
+ with:
+ poetry-version: "2.1.3"
- name: "Get image version"
- run: "echo INVOKE_PARSER_IMAGE_VER=`poetry version -s`-py$${{ matrix.python-version }} >> $GITHUB_ENV"
+ run: "echo INVOKE_PARSER_IMAGE_VER=`poetry version -s`-py${{ matrix.python-version }} >> $GITHUB_ENV"
- name: "Set up Docker Buildx"
id: "buildx"
- uses: "docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2" # v3.10.0
+ uses: "docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2" # v3.10.0
- name: "Build"
- uses: "docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25" # v5.4.0
+ uses: "docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25" # v5.4.0
with:
builder: "${{ steps.buildx.outputs.name }}"
context: "./"
@@ -120,33 +118,33 @@ jobs:
cache-from: "type=gha,scope=${{ env.INVOKE_PARSER_IMAGE_NAME }}-${{ env.INVOKE_PARSER_IMAGE_VER }}-py${{ matrix.python-version }}"
cache-to: "type=gha,scope=${{ env.INVOKE_PARSER_IMAGE_NAME }}-${{ env.INVOKE_PARSER_IMAGE_VER }}-py${{ matrix.python-version }}"
build-args: |
- PYTHON_VER=${{ env.PYTHON_VER }}
- - name: "Debug: Show docker images"
- run: "docker image ls"
+ PYTHON_VER=${{ matrix.python-version }}
- name: "Linting: Pylint"
run: "poetry run invoke pylint"
- needs:
- - "poetry"
pytest:
+ needs:
+ - "check-in-docker"
strategy:
fail-fast: true
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
- runs-on: "ubuntu-24.04"
+ runs-on: "ubuntu-latest"
env:
- PYTHON_VER: "${{ matrix.python-version }}"
+ INVOKE_PARSER_PYTHON_VER: "${{ matrix.python-version }}"
steps:
- name: "Check out repository code"
uses: "actions/checkout@v4"
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v6"
+ with:
+ poetry-version: "2.1.3"
- name: "Get image version"
run: "echo INVOKE_PARSER_IMAGE_VER=`poetry version -s`-py${{ matrix.python-version }} >> $GITHUB_ENV"
- name: "Set up Docker Buildx"
id: "buildx"
- uses: "docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2" # v3.10.0
+ uses: "docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2" # v3.10.0
- name: "Build"
- uses: "docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25" # v5.4.0
+ uses: "docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25" # v5.4.0
with:
builder: "${{ steps.buildx.outputs.name }}"
context: "./"
@@ -157,46 +155,14 @@ jobs:
cache-from: "type=gha,scope=${{ env.INVOKE_PARSER_IMAGE_NAME }}-${{ env.INVOKE_PARSER_IMAGE_VER }}-py${{ matrix.python-version }}"
cache-to: "type=gha,scope=${{ env.INVOKE_PARSER_IMAGE_NAME }}-${{ env.INVOKE_PARSER_IMAGE_VER }}-py${{ matrix.python-version }}"
build-args: |
- PYTHON_VER=${{ env.PYTHON_VER }}
- - name: "Debug: Show docker images"
- run: "docker image ls"
+ PYTHON_VER=${{ matrix.python-version }}
- name: "Run Tests"
run: "poetry run invoke pytest"
- needs:
- - "poetry"
- publish_gh:
- name: "Publish to GitHub"
- runs-on: "ubuntu-24.04"
- # yamllint disable-line rule:quoted-strings
- if: startsWith(github.ref, 'refs/tags/v')
- steps:
- - name: "Check out repository code"
- uses: "actions/checkout@v4"
- - name: "Set up Python"
- uses: "actions/setup-python@v5"
- with:
- python-version: "3.10"
- - name: "Install Python Packages"
- run: "pip install poetry"
- - name: "Set env"
- run: "echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV"
- - name: "Run Poetry Version"
- run: "poetry version $RELEASE_VERSION"
- - name: "Build Documentation"
- run: "poetry run invoke build-and-check-docs"
- - name: "Run Poetry Build"
- run: "poetry build"
- - name: "Upload binaries to release"
- run: "gh release upload ${{ github.ref_name }} dist/*.{tar.gz,whl}"
- env:
- GH_TOKEN: "${{ secrets.NTC_GITHUB_TOKEN }}"
- needs:
- - "pytest"
changelog:
if: >
contains(fromJson('["develop"]'), github.base_ref) &&
(github.head_ref != 'main') && (!startsWith(github.head_ref, 'release'))
- runs-on: "ubuntu-22.04"
+ runs-on: "ubuntu-latest"
steps:
- name: "Check out repository code"
uses: "actions/checkout@v4"
@@ -205,70 +171,8 @@ jobs:
- name: "Setup environment"
uses: "networktocode/gh-action-setup-poetry-environment@v6"
with:
- poetry-version: "1.8.5"
+ poetry-version: "2.1.3"
- name: "Check for changelog entry"
run: |
git fetch --no-tags origin +refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}
poetry run towncrier check --compare-with origin/${{ github.base_ref }}
- publish_pypi:
- name: "Push Package to PyPI"
- runs-on: "ubuntu-24.04"
- # yamllint disable-line rule:quoted-strings
- if: startsWith(github.ref, 'refs/tags/v')
- steps:
- - name: "Check out repository code"
- uses: "actions/checkout@v4"
- - name: "Set up Python"
- uses: "actions/setup-python@v5"
- with:
- python-version: "3.10"
- - name: "Install Python Packages"
- run: "pip install poetry"
- - name: "Set env"
- run: "echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV"
- - name: "Run Poetry Version"
- run: "poetry version $RELEASE_VERSION"
- - name: "Run Poetry Build"
- run: "poetry build"
- - name: "Push to PyPI"
- uses: "pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc" # v1.12.4
- with:
- user: "__token__"
- password: "${{ secrets.PYPI_API_TOKEN }}"
- needs:
- - "pytest"
- slack-notify:
- needs:
- - "publish_gh"
- - "publish_pypi"
- runs-on: "ubuntu-24.04"
- env:
- SLACK_WEBHOOK_URL: "${{ secrets.SLACK_WEBHOOK_URL }}"
- SLACK_MESSAGE: >-
- *NOTIFICATION: NEW-RELEASE-PUBLISHED*\n
- Repository: <${{ github.server_url }}/${{ github.repository }}|${{ github.repository }}>\n
- Release: <${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }}|${{ github.ref_name }}>\n
- Published by: <${{ github.server_url }}/${{ github.actor }}|${{ github.actor }}>
- steps:
- - name: "Send a notification to Slack"
- # ENVs cannot be used directly in job.if. This is a workaround to check
- # if SLACK_WEBHOOK_URL is present.
- if: "env.SLACK_WEBHOOK_URL != ''"
- uses: "slackapi/slack-github-action@fcfb566f8b0aab22203f066d80ca1d7e4b5d05b3" # v1.27.1
- with:
- payload: |
- {
- "text": "${{ env.SLACK_MESSAGE }}",
- "blocks": [
- {
- "type": "section",
- "text": {
- "type": "mrkdwn",
- "text": "${{ env.SLACK_MESSAGE }}"
- }
- }
- ]
- }
- env:
- SLACK_WEBHOOK_URL: "${{ secrets.SLACK_WEBHOOK_URL }}"
- SLACK_WEBHOOK_TYPE: "INCOMING_WEBHOOK"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..e31b6a74
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,106 @@
+---
+name: "Release"
+on: # yamllint disable-line rule:truthy rule:comments
+ release:
+ types: ["published"]
+
+jobs:
+ build:
+ name: "Build package with poetry"
+ runs-on: "ubuntu-latest"
+ if: "startsWith(github.ref, 'refs/tags/v')"
+ steps:
+ - uses: "actions/checkout@v4"
+ - name: "Setup environment"
+ uses: "networktocode/gh-action-setup-poetry-environment@v6"
+ with:
+ poetry-version: "2.1.3"
+ python-version: "3.13"
+ poetry-install-options: "--no-root"
+ - name: "Run Poetry Build"
+ run: "poetry build"
+
+ - name: "Check that the release tag matches the version in pyproject.toml"
+ run: |
+ if [ "${{ github.ref_name }}" != "v$(poetry version -s)" ]; then exit 1; fi
+
+ - uses: "actions/upload-artifact@v4"
+ with:
+ name: "distfiles"
+ path: "dist/"
+ if-no-files-found: "error"
+
+ publish-github:
+ name: "Publish to GitHub"
+ runs-on: "ubuntu-latest"
+ if: "startsWith(github.ref, 'refs/tags/v')"
+ permissions:
+ contents: "write"
+ needs: "build"
+ steps:
+ - uses: "actions/checkout@v4"
+ - name: "Retrieve built package from cache"
+ uses: "actions/download-artifact@v4"
+ with:
+ name: "distfiles"
+ path: "dist/"
+
+ - name: "Upload binaries to release"
+ run: "gh release upload ${{ github.ref_name }} dist/*.{tar.gz,whl}"
+ env:
+ GH_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
+
+ publish-pypi:
+ name: "Push Package to PyPI"
+ runs-on: "ubuntu-latest"
+ if: "startsWith(github.ref, 'refs/tags/v')"
+ needs: "build"
+ environment: "pypi"
+ # Steps to publish to PyPI.
+ steps:
+ - name: "Retrieve built package from cache"
+ uses: "actions/download-artifact@v4"
+ with:
+ name: "distfiles"
+ path: "dist/"
+ - name: "Publish package distributions to PyPI"
+ uses: "pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e" # v1.13.0
+ ## Used for networktocode org since trusted publisher isn't supported for GitHub Plan.
+ with:
+ user: "__token__"
+ password: "${{ secrets.PYPI_API_TOKEN }}"
+ # End publish to PyPI job.
+
+ slack-notify:
+ needs:
+ - "publish-github"
+ - "publish-pypi"
+ runs-on: "ubuntu-latest"
+ env:
+ # Secrets cannot be directly referenced in if: conditionals. They must be set as a job env var first.
+ # Ref: https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#example-using-secrets
+ SLACK_WEBHOOK_URL: "${{ secrets.OSS_PYPI_SLACK_WEBHOOK_URL }}"
+ SLACK_WEBHOOK_TYPE: "INCOMING_WEBHOOK"
+ SLACK_MESSAGE: >-
+ *NOTIFICATION: NEW-RELEASE-PUBLISHED*\n
+ Repository: <${{ github.server_url }}/${{ github.repository }}|${{ github.repository }}>\n
+ Release: <${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }}|${{ github.ref_name }}>\n
+ Published by: <${{ github.server_url }}/${{ github.actor }}|${{ github.actor }}>
+ steps:
+ - name: "Send a notification to Slack"
+ if: "${{ env.SLACK_WEBHOOK_URL != '' }}"
+ uses: "slackapi/slack-github-action@fcfb566f8b0aab22203f066d80ca1d7e4b5d05b3" # v1.27.1
+ with:
+ payload: |
+ {
+ "text": "${{ env.SLACK_MESSAGE }}",
+ "blocks": [
+ {
+ "type": "section",
+ "text": {
+ "type": "mrkdwn",
+ "text": "${{ env.SLACK_MESSAGE }}"
+ }
+ }
+ ]
+ }
diff --git a/.gitignore b/.gitignore
index f539d853..bb324eae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -125,6 +125,7 @@ venv.bak/
# mkdocs documentation
/site
+/circuit-maintenance-parser/static/
# mypy
.mypy_cache/
@@ -294,3 +295,17 @@ fabric.properties
### vscode ###
.vscode/*
*.code-workspace
+
+# Rando
+creds.env
+development/*.txt
+
+# Invoke overrides
+invoke.yml
+
+# Docs
+docs/README.md
+docs/CHANGELOG.md
+public
+/compose.yaml
+/dump.sql
diff --git a/.readthedocs.yml b/.readthedocs.yml
new file mode 100644
index 00000000..36f32f00
--- /dev/null
+++ b/.readthedocs.yml
@@ -0,0 +1,27 @@
+---
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Setup the build environment.
+build:
+ os: "ubuntu-lts-latest"
+ tools:
+ python: "3.13"
+ jobs:
+ post_install:
+ # Install poetry
+ # https://python-poetry.org/docs/#installing-manually
+ - "pip install poetry"
+ # Install dependencies 'docs' dependency group
+ # https://python-poetry.org/docs/managing-dependencies/#dependency-groups
+ # VIRTUAL_ENV needs to be set manually for now.
+ # See https://github.com/readthedocs/readthedocs.org/pull/11152/
+ - "VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --only docs"
+
+mkdocs:
+ configuration: "mkdocs.yml"
+ fail_on_warning: true
diff --git a/Dockerfile b/Dockerfile
index 06cdf467..3c1fdca0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-ARG PYTHON_VER="3.9"
+ARG PYTHON_VER="3.10"
FROM python:${PYTHON_VER}-slim
@@ -8,7 +8,7 @@ FROM python:${PYTHON_VER}-slim
# This also makes it so that Poetry will *not* be included in the "final" image since it's not installed to /usr/local/
ARG POETRY_HOME=/opt/poetry
ARG POETRY_INSTALLER_PARALLEL=true
-ARG POETRY_VERSION=1.8.2
+ARG POETRY_VERSION=2.1.3
ARG POETRY_VIRTUALENVS_CREATE=false
ADD https://install.python-poetry.org /tmp/install-poetry.py
RUN python /tmp/install-poetry.py
diff --git a/LICENSE b/LICENSE
index f433b1a5..9e40eee1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,177 +1,15 @@
+Apache Software License 2.0
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
+Copyright (c) 2021-2026, Network to Code, LLC
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+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
- 1. Definitions.
+http://www.apache.org/licenses/LICENSE-2.0
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
+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.
diff --git a/README.md b/README.md
index 71ef76fd..ee5dcae2 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,24 @@
# circuit-maintenance-parser
+
+
+
+
+
+
+
`circuit-maintenance-parser` is a Python library that parses circuit maintenance notifications from Network Service Providers (NSPs), converting heterogeneous formats to a well-defined structured format.
+## Documentation
+
+Full documentation for this library can be found over on the [Circuit-Maintenance-Parser Docs](https://circuit-maintenance-parser.readthedocs.io/) website:
+
+- [User Guide](https://circuit-maintenance-parser.readthedocs.io/en/latest/user/lib_overview/) - Overview, Using the Library, Getting Started.
+- [Administrator Guide](https://circuit-maintenance-parser.readthedocs.io/en/latest/admin/install/) - How to Install, Configure, Upgrade, or Uninstall the Library.
+- [Developer Guide](https://circuit-maintenance-parser.readthedocs.io/en/latest/dev/contributing/) - Extending the Library, Code Reference, Contribution Guide.
+- [Release Notes / Changelog](https://circuit-maintenance-parser.readthedocs.io/en/latest/admin/release_notes/).
+- [Frequently Asked Questions](https://circuit-maintenance-parser.readthedocs.io/en/latest/user/faq/).
+
## Context
Every network depends on external circuits provided by NSPs who interconnect them to the Internet, to office branches or to
@@ -280,158 +297,21 @@ Circuit Maintenance Notification #0
}
```
-## How to Extend the Library?
-
-Even though the library aims to include support for as many providers as possible, it's likely that not all the thousands of NSP are supported and you may need to add support for some new one. Adding a new `Provider` is quite straightforward, and in the following example we are adding support for an imaginary provider, ABCDE, that uses HTML notifications.
-
-The first step is creating a new file: `circuit_maintenance_parser/parsers/abcde.py`. This file will contain all the custom parsers needed for the provider and it will import the base classes for each parser type from `circuit_maintenance_parser.parser`. In the example, we only need to import `Html` and in the child class implement the methods required by the class, in this case `parse_html()` which will return a `dict` with all the data that this `Parser` can extract. In this case, we have to helper methods, `_parse_bs` and `_parse_tables` that implement the logic to navigate the notification data.
-
-```python
-from typing import Dict
-import bs4 # type: ignore
-from bs4.element import ResultSet # type: ignore
-from circuit_maintenance_parser.parser import Html
-
-class HtmlParserABCDE1(Html):
- def parse_html(self, soup: ResultSet) -> Dict:
- data = {}
- self._parse_bs(soup.find_all("b"), data)
- self._parse_tables(soup.find_all("table"), data)
- return [data]
-
- def _parse_bs(self, btags: ResultSet, data: Dict):
- ...
-
- def _parse_tables(self, tables: ResultSet, data: Dict):
- ...
-```
-
-The next step is to create the new `Provider` by defining a new class in `circuit_maintenance_parser/provider.py`. This class that inherits from `GenericProvider` only needs to define two attributes:
-
-- `_processors`: is a `list` of `Processor` instances that uses several data `Parsers`. In this example, we don't need to create a new custom `Processor` because the combined logic serves well (the most likely case), and we only need to use the newly defined `HtmlParserABCDE1` and also the generic `EmailDateParser` that extracts the email date. Also notice that you could have multiple `Processors` with different `Parsers` in this list, supporting several formats.
-- `_default_organizer`: This is a default helper to fill the `organizer` attribute in the `Maintenance` if the information is not part of the original notification.
-
-```python
-class ABCDE(GenericProvider):
- _processors: List[GenericProcessor] = [
- CombinedProcessor(data_parsers=[EmailDateParser, HtmlParserABCDE1]),
- ]
- _default_organizer = "noc@abcde.com"
-```
-
-And expose the new `Provider` in `circuit_maintenance_parser/__init__.py`:
-
-```python
-from .provider import (
- GenericProvider,
- ABCDE,
- ...
-)
-
-SUPPORTED_PROVIDERS = (
- GenericProvider,
- ABCDE,
- ...
-)
-```
+## Contributing
-Last, but not least, you should update the tests!
-
-- Test the new `Parser` in `tests/unit/test_parsers.py`
-- Test the new `Provider` logic in `tests/unit/test_e2e.py`
-
-... adding the necessary data samples in `tests/unit/data/abcde/`.
-
-> You can anonymize your IPv4 and IPv6 addresses using the `invoke anonymize-ips`. Keep in mind that only IPv4 addresses for documentation purposes (RFC5737: "192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24") are preserved, in case you need to check these IPs in your test output (unlikely)
-
-# Contributing
-
-Pull requests are welcomed and automatically built and tested against multiple versions of Python through Travis CI.
+Pull requests are welcomed and automatically built and tested through GitHub Actions.
The project is following Network to Code software development guidelines and is leveraging:
-- Black, Pylint, Mypy, Bandit and pydocstyle for Python linting and formatting.
+- Ruff for Python linting and formatting.
+- Pylint for additional static analysis.
- Unit and integration tests to ensure the library is working properly.
-## Local Development
-
-### Requirements
-
-- Install `poetry`
-- Install dependencies and library locally: `poetry install`
-- Run CI tests locally: `invoke tests`
-
-> Note: you can run the tasks without Docker by setting the environment variable `INVOKE_PARSER_LOCAL=True`. This will run the tasks directly on your local machine instead of inside a Docker container.
-
-### How to add a new Circuit Maintenance provider?
-
-1. Define the `Parsers`(inheriting from some of the generic `Parsers` or a new one) that will extract the data from the notification, which could contain multiple `DataParts`. The `data_type` of the `Parser` and the `DataPart` have to match. The custom `Parsers` will be placed in the `parsers` folder.
-2. Update the `unit/test_parsers.py` with the new parsers, providing some data to test and validate the extracted data.
-3. Define a new `Provider` inheriting from the `GenericProvider`, defining the `Processors` and the respective `Parsers` to be used. Maybe you can reuse some of the generic `Processors` or maybe you will need to create a custom one. If this is the case, place it in the `processors` folder.
- - The `Provider` also supports the definition of a `_include_filter` and a `_exclude_filter` to limit the notifications that are actually processed, avoiding false positive errors for notification that are not relevant.
-4. Update the `unit/test_e2e.py` with the new provider, providing some data to test and validate the final `Maintenances` created.
-5. **Expose the new `Provider` class** updating the map `SUPPORTED_PROVIDERS` in `circuit_maintenance_parser/__init__.py` to officially expose the `Provider`.
-6. You can run some tests here to verify that your new unit tests do not cause issues with existing tests, and in general they work as expected. You can do this by running `pytest --log-cli-level=DEBUG --capture=tee-sys`. You can narrow down the tests that you want to execute with the `-k` flag. If successful, your results should look similar to the following:
-
-```
--> % pytest --log-cli-level=DEBUG --capture=tee-sys -k test_parsers
-...omitted debug logs...
-====================================================== 99 passed, 174 deselected, 17 warnings in 10.35s ======================================================
-```
-
-7. Run some final CI tests locally to ensure that there is no linting/formatting issues with your changes. You should look to get a code score of 10/10. See the example below: `invoke tests`
-
-```
--> % poetry run invoke tests
-DOCKER - Running command: ruff format --check . container: circuit_maintenance_parser:latest
-52 files already formatted
-DOCKER - Running command: ruff check --output-format concise . container: circuit_maintenance_parser:latest
-All checks passed!
-DOCKER - Running command: find . -name "*.py" | grep -vE "tests/unit" | xargs pylint container: circuit_maintenance_parser:latest
-
-------------------------------------
-Your code has been rated at 10.00/10
-```
-
-### How to debug circuit-maintenance-parser library locally
-
-1. `poetry install` updates the library and its dependencies locally.
-2. `circuit-maintenance-parser` is now built with your recent local changes.
-
-If you were to add loggers or debuggers to one of the classes:
-
-```python
-class HtmlParserZayo1(Html):
- def parse_bs(self, btags: ResultSet, data: dict):
- """Parse B tag."""
- raise Exception('Debugging exception')
-```
-
-After running `poetry install`:
-
-```
--> % circuit-maintenance-parser --data-file ~/Downloads/zayo.eml --data-type email --provider-type zayo
-Provider processing failed: Failed creating Maintenance notification for Zayo.
-Details:
-- Processor CombinedProcessor from Zayo failed due to: Debugging exception
-```
-
-> Note: `invoke build` will result in an error due to no Dockerfile. This is expected as the library runs simple pytest testing without a container.
-
-```
--> % invoke build
-Building image circuit-maintenance-parser:2.2.2-py3.8
-#1 [internal] load build definition from Dockerfile
-#1 transferring dockerfile: 2B done
-#1 DONE 0.0s
-WARNING: failed to get git remote url: fatal: No remote configured to list refs from.
-ERROR: failed to solve: rpc error: code = Unknown desc = failed to solve with frontend dockerfile.v0: failed to read dockerfile: open /var/lib/docker/tmp/buildkit-mount1243547759/Dockerfile: no such file or directory
-```
+For more details, see the [Contributing Guide](https://circuit-maintenance-parser.readthedocs.io/en/latest/dev/contributing/) and [Development Environment Guide](https://circuit-maintenance-parser.readthedocs.io/en/latest/dev/dev_environment/).
## Questions
-For any questions or comments, please check the [FAQ](FAQ.md) first and feel free to swing by the [Network to Code slack channel](https://networktocode.slack.com/) (channel #networktocode).
-Sign up [here](http://slack.networktocode.com/)
+For any questions or comments, please check the [FAQ](https://circuit-maintenance-parser.readthedocs.io/en/latest/user/faq/) first. Feel free to also swing by the [Network to Code Slack](https://networktocode.slack.com/) (channel `#networktocode`), sign up [here](http://slack.networktocode.com/) if you don't have an account.
## License notes
diff --git a/changes/+main.housekeeping b/changes/+main.housekeeping
new file mode 100644
index 00000000..3433adf6
--- /dev/null
+++ b/changes/+main.housekeeping
@@ -0,0 +1 @@
+Rebaked from the cookie `main`.
diff --git a/changes/.gitignore b/changes/.gitignore
new file mode 100644
index 00000000..f935021a
--- /dev/null
+++ b/changes/.gitignore
@@ -0,0 +1 @@
+!.gitignore
diff --git a/circuit_maintenance_parser/__init__.py b/circuit_maintenance_parser/__init__.py
index 0d7dc878..bc994864 100644
--- a/circuit_maintenance_parser/__init__.py
+++ b/circuit_maintenance_parser/__init__.py
@@ -1,5 +1,6 @@
"""Circuit-maintenance-parser init."""
+from importlib import metadata
from typing import Optional, Type
from .data import NotificationData
@@ -75,6 +76,9 @@
Zayo,
)
+
+__version__ = metadata.version(__name__)
+
SUPPORTED_PROVIDER_NAMES = [provider.get_provider_type() for provider in SUPPORTED_PROVIDERS]
SUPPORTED_ORGANIZER_EMAILS = [provider.get_default_organizer() for provider in SUPPORTED_PROVIDERS]
diff --git a/circuit_maintenance_parser/cli.py b/circuit_maintenance_parser/cli.py
index 69fbe9a4..08274682 100644
--- a/circuit_maintenance_parser/cli.py
+++ b/circuit_maintenance_parser/cli.py
@@ -13,7 +13,12 @@
@click.command()
@click.option("--data-file", required=True, help="File containing raw data to parse.")
-@click.option("--data-type", required=False, help="Type of notification data. Default: Icalendar", default="ical")
+@click.option(
+ "--data-type",
+ required=False,
+ help="Type of notification data. Default: Icalendar",
+ default="ical",
+)
@click.option(
"--provider-type",
type=click.Choice([provider.get_provider_type() for provider in SUPPORTED_PROVIDERS]),
diff --git a/docs/admin/install.md b/docs/admin/install.md
new file mode 100644
index 00000000..12922649
--- /dev/null
+++ b/docs/admin/install.md
@@ -0,0 +1,22 @@
+# Installation
+
+Option 1: Install from PyPI.
+
+```bash
+pip install circuit-maintenance-parser
+```
+
+Option 2: Manually install via Poetry.
+
+```bash
+git clone https://github.com/networktocode/circuit-maintenance-parser.git
+cd circuit-maintenance-parser
+curl -sSL https://install.python-poetry.org | python3 -
+poetry install
+```
+
+Option 3: Install from a GitHub branch, such as develop as shown below.
+
+```bash
+pip install git+https://github.com/networktocode/circuit-maintenance-parser.git@develop
+```
diff --git a/docs/admin/release_notes/index.md b/docs/admin/release_notes/index.md
new file mode 100644
index 00000000..12cb5169
--- /dev/null
+++ b/docs/admin/release_notes/index.md
@@ -0,0 +1,3 @@
+# Release Notes
+
+All the published release notes can be found via the navigation menu. All patch releases are included in the same minor release (e.g. `v1.2`) document.
diff --git a/docs/admin/release_notes/version_1.0.md b/docs/admin/release_notes/version_1.0.md
new file mode 100644
index 00000000..d87d2b1b
--- /dev/null
+++ b/docs/admin/release_notes/version_1.0.md
@@ -0,0 +1,15 @@
+# v1.0 Release Notes
+
+This document describes all new features and changes in the `1.x` release series. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+For a complete history of all releases, see the [full changelog](../../release_notes.md).
+
+## v1.0.2 - 2021-05-05
+
+### Added
+
+- [#10](https://github.com/networktocode/circuit-maintenance-parser/pull/10) - Added `cli` command to run as a script.
+
+## v1.0.0 - 2021-04-29
+
+Initial release of the circuit-maintenance-parser library with support for parsing circuit maintenance notifications from Network Service Providers using the iCalendar BCOP standard format.
diff --git a/docs/admin/release_notes/version_2.10.md b/docs/admin/release_notes/version_2.10.md
new file mode 100644
index 00000000..e0b2ea78
--- /dev/null
+++ b/docs/admin/release_notes/version_2.10.md
@@ -0,0 +1,19 @@
+# v2.10 Release Notes
+
+This document describes all new features and changes in the `2.10` release series. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [v2.10.0 (2026-01-27)](https://github.com/networktocode/circuit-maintenance-parser/releases/tag/v2.10.0)
+
+### Added
+
+- [#330](https://github.com/networktocode/circuit-maintenance-parser/issues/330) - Add support for parsing AWS HTML format maintenance notification emails
+
+### Dependencies
+
+- [#360](https://github.com/networktocode/circuit-maintenance-parser/issues/360) - Updated lxml to include version 6
+- [#361](https://github.com/networktocode/circuit-maintenance-parser/issues/361) - Updated timezonefinder to v8.2.0
+- [#345](https://github.com/networktocode/circuit_maintenance_parser/issues/345) - Updated minimum Python version to 3.10 and upgraded dependencies including pytest (9.0), pylint (4.0), towncrier (25.8), backoff (2.2), and type stubs
+
+### Housekeeping
+
+- [#344](https://github.com/networktocode/circuit-maintenance-parser/issues/344) - Updated test coverage for the codebase
diff --git a/docs/admin/uninstall.md b/docs/admin/uninstall.md
new file mode 100644
index 00000000..0d90822d
--- /dev/null
+++ b/docs/admin/uninstall.md
@@ -0,0 +1,7 @@
+# Uninstall
+
+Uninstall from environment.
+
+```bash
+pip uninstall circuit-maintenance-parser
+```
diff --git a/docs/admin/upgrade.md b/docs/admin/upgrade.md
new file mode 100644
index 00000000..e30f6919
--- /dev/null
+++ b/docs/admin/upgrade.md
@@ -0,0 +1,7 @@
+# Upgrading the Library
+
+Upgrade from PyPI.
+
+```bash
+pip install circuit-maintenance-parser --upgrade
+```
diff --git a/docs/assets/extra.css b/docs/assets/extra.css
new file mode 100644
index 00000000..50884f4a
--- /dev/null
+++ b/docs/assets/extra.css
@@ -0,0 +1,152 @@
+:root>* {
+ --md-accent-fg-color: #ff8504;
+ --md-primary-fg-color: #ff8504;
+ --md-typeset-a-color: #0097ff;
+}
+
+[data-md-color-scheme="slate"] {
+ --md-default-bg-color: hsla(var(--md-hue), 0%, 15%, 1);
+ --md-typeset-a-color: #0097ff;
+}
+
+/* Accessibility: Increase fonts for dark theme */
+[data-md-color-scheme="slate"] .md-typeset {
+ font-size: 0.9rem;
+}
+
+[data-md-color-scheme="slate"] .md-typeset table:not([class]) {
+ font-size: 0.7rem;
+}
+
+.md-tabs__link {
+ font-size: 0.8rem;
+}
+
+.md-tabs__link--active {
+ color: var(--md-primary-fg-color);
+}
+
+.md-header__button.md-logo :is(img, svg) {
+ height: 2rem;
+}
+
+.md-header__button.md-logo :-webkit-any(img, svg) {
+ height: 2rem;
+}
+
+.md-header__title {
+ font-size: 1.2rem;
+}
+
+img.logo {
+ height: 100px;
+}
+
+img.copyright-logo {
+ height: 24px;
+ vertical-align: middle;
+}
+
+[data-md-color-primary=black] .md-header {
+ background-color: #212121;
+}
+
+@media screen and (min-width: 76.25em) {
+ [data-md-color-primary=black] .md-tabs {
+ background-color: #212121;
+ }
+}
+
+/* Customization for mkdocstrings */
+/* Indentation. */
+div.doc-contents:not(.first) {
+ padding-left: 25px;
+ border-left: .2rem solid var(--md-typeset-table-color);
+}
+
+/* Mark external links as such. */
+a.autorefs-external::after {
+ /* https://primer.style/octicons/arrow-up-right-24 */
+ background-image: url('data:image/svg+xml,');
+ content: ' ';
+
+ display: inline-block;
+ position: relative;
+ top: 0.1em;
+ margin-left: 0.2em;
+ margin-right: 0.1em;
+
+ height: 1em;
+ width: 1em;
+ border-radius: 100%;
+ background-color: var(--md-typeset-a-color);
+}
+
+a.autorefs-external:hover::after {
+ background-color: var(--md-accent-fg-color);
+}
+
+
+/* Customization for mkdocs-version-annotations */
+:root {
+ /* Icon for "version-added" admonition: Material Design Icons "plus-box-outline" */
+ --md-admonition-icon--version-added: url('data:image/svg+xml;charset=utf-8,');
+ /* Icon for "version-changed" admonition: Material Design Icons "delta" */
+ --md-admonition-icon--version-changed: url('data:image/svg+xml;charset=utf-8,');
+ /* Icon for "version-removed" admonition: Material Design Icons "minus-circle-outline" */
+ --md-admonition-icon--version-removed: url('data:image/svg+xml;charset=utf-8,');
+}
+
+/* "version-added" admonition in green */
+.md-typeset .admonition.version-added,
+.md-typeset details.version-added {
+ border-color: rgb(0, 200, 83);
+}
+
+.md-typeset .version-added>.admonition-title,
+.md-typeset .version-added>summary {
+ background-color: rgba(0, 200, 83, .1);
+}
+
+.md-typeset .version-added>.admonition-title::before,
+.md-typeset .version-added>summary::before {
+ background-color: rgb(0, 200, 83);
+ -webkit-mask-image: var(--md-admonition-icon--version-added);
+ mask-image: var(--md-admonition-icon--version-added);
+}
+
+/* "version-changed" admonition in orange */
+.md-typeset .admonition.version-changed,
+.md-typeset details.version-changed {
+ border-color: rgb(255, 145, 0);
+}
+
+.md-typeset .version-changed>.admonition-title,
+.md-typeset .version-changed>summary {
+ background-color: rgba(255, 145, 0, .1);
+}
+
+.md-typeset .version-changed>.admonition-title::before,
+.md-typeset .version-changed>summary::before {
+ background-color: rgb(255, 145, 0);
+ -webkit-mask-image: var(--md-admonition-icon--version-changed);
+ mask-image: var(--md-admonition-icon--version-changed);
+}
+
+/* "version-removed" admonition in red */
+.md-typeset .admonition.version-removed,
+.md-typeset details.version-removed {
+ border-color: rgb(255, 82, 82);
+}
+
+.md-typeset .version-removed>.admonition-title,
+.md-typeset .version-removed>summary {
+ background-color: rgba(255, 82, 82, .1);
+}
+
+.md-typeset .version-removed>.admonition-title::before,
+.md-typeset .version-removed>summary::before {
+ background-color: rgb(255, 82, 82);
+ -webkit-mask-image: var(--md-admonition-icon--version-removed);
+ mask-image: var(--md-admonition-icon--version-removed);
+}
diff --git a/docs/assets/favicon.ico b/docs/assets/favicon.ico
new file mode 100644
index 00000000..9685c83b
Binary files /dev/null and b/docs/assets/favicon.ico differ
diff --git a/docs/assets/networktocode_bw.png b/docs/assets/networktocode_bw.png
new file mode 100644
index 00000000..075c4926
Binary files /dev/null and b/docs/assets/networktocode_bw.png differ
diff --git a/docs/assets/networktocode_logo.png b/docs/assets/networktocode_logo.png
new file mode 100644
index 00000000..b2a3cba2
Binary files /dev/null and b/docs/assets/networktocode_logo.png differ
diff --git a/docs/assets/overrides/partials/copyright.html b/docs/assets/overrides/partials/copyright.html
new file mode 100644
index 00000000..cbf6bde5
--- /dev/null
+++ b/docs/assets/overrides/partials/copyright.html
@@ -0,0 +1,21 @@
+
+