amati 0.1.1__tar.gz → 0.2.1__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 (96) hide show
  1. amati-0.2.1/.dockerignore +1 -0
  2. {amati-0.1.1 → amati-0.2.1}/.github/dependabot.yml +6 -2
  3. amati-0.2.1/.github/workflows/checks.yaml +90 -0
  4. amati-0.2.1/.github/workflows/publish.yaml +47 -0
  5. amati-0.2.1/.python-version +1 -0
  6. amati-0.2.1/Dockerfile +16 -0
  7. amati-0.2.1/PKG-INFO +151 -0
  8. amati-0.2.1/README.md +127 -0
  9. amati-0.2.1/TEMPLATE.html +116 -0
  10. {amati-0.1.1 → amati-0.2.1}/amati/__init__.py +0 -1
  11. amati-0.2.1/amati/_error_handler.py +48 -0
  12. {amati-0.1.1 → amati-0.2.1}/amati/amati.py +86 -20
  13. {amati-0.1.1 → amati-0.2.1}/amati/exceptions.py +2 -4
  14. {amati-0.1.1 → amati-0.2.1}/amati/fields/email.py +3 -7
  15. {amati-0.1.1 → amati-0.2.1}/amati/fields/http_status_codes.py +4 -5
  16. {amati-0.1.1 → amati-0.2.1}/amati/fields/iso9110.py +4 -5
  17. {amati-0.1.1 → amati-0.2.1}/amati/fields/media.py +3 -7
  18. {amati-0.1.1 → amati-0.2.1}/amati/fields/oas.py +6 -12
  19. {amati-0.1.1 → amati-0.2.1}/amati/fields/spdx_licences.py +4 -7
  20. {amati-0.1.1 → amati-0.2.1}/amati/fields/uri.py +3 -11
  21. {amati-0.1.1 → amati-0.2.1}/amati/logging.py +8 -8
  22. {amati-0.1.1 → amati-0.2.1}/amati/model_validators.py +42 -33
  23. {amati-0.1.1 → amati-0.2.1}/amati/validators/generic.py +18 -13
  24. {amati-0.1.1 → amati-0.2.1}/amati/validators/oas304.py +93 -130
  25. {amati-0.1.1 → amati-0.2.1}/amati/validators/oas311.py +58 -156
  26. amati-0.2.1/bin/checks.sh +10 -0
  27. {amati-0.1.1 → amati-0.2.1}/pyproject.toml +2 -1
  28. {amati-0.1.1 → amati-0.2.1}/tests/data/.amati.tests.yaml +22 -11
  29. amati-0.1.1/tests/data/DigitalOcean-public.v2.errors → amati-0.2.1/tests/data/DigitalOcean-public.v2.errors.json +0 -13
  30. amati-0.2.1/tests/data/api.github.com.yaml.errors.json +23 -0
  31. amati-0.2.1/tests/data/next-api.github.com.yaml.errors.json +32 -0
  32. amati-0.2.1/tests/data/redocly.openapi.yaml.errors.json +23 -0
  33. {amati-0.1.1 → amati-0.2.1}/tests/fields/test_email.py +1 -2
  34. {amati-0.1.1 → amati-0.2.1}/tests/fields/test_http_status_codes.py +6 -1
  35. {amati-0.1.1 → amati-0.2.1}/tests/fields/test_media.py +1 -1
  36. {amati-0.1.1 → amati-0.2.1}/tests/model_validators/test_all_of.py +3 -4
  37. {amati-0.1.1 → amati-0.2.1}/tests/model_validators/test_at_least_one.py +3 -4
  38. {amati-0.1.1 → amati-0.2.1}/tests/model_validators/test_if_then.py +18 -17
  39. {amati-0.1.1 → amati-0.2.1}/tests/model_validators/test_only_one.py +3 -4
  40. amati-0.2.1/tests/test_external_specs.py +93 -0
  41. amati-0.2.1/tests/test_logging.py +31 -0
  42. {amati-0.1.1 → amati-0.2.1}/tests/validators/test_generic.py +6 -4
  43. {amati-0.1.1 → amati-0.2.1}/tests/validators/test_licence_object.py +10 -10
  44. {amati-0.1.1 → amati-0.2.1}/tests/validators/test_security_scheme_object.py +4 -4
  45. {amati-0.1.1 → amati-0.2.1}/tests/validators/test_server_variable_object.py +2 -2
  46. {amati-0.1.1 → amati-0.2.1}/uv.lock +84 -42
  47. amati-0.1.1/.github/workflows/checks.yaml +0 -64
  48. amati-0.1.1/.github/workflows/publish.yaml +0 -29
  49. amati-0.1.1/.python-version +0 -1
  50. amati-0.1.1/PKG-INFO +0 -89
  51. amati-0.1.1/README.md +0 -66
  52. amati-0.1.1/amati/references.py +0 -33
  53. amati-0.1.1/bin/checks.sh +0 -6
  54. amati-0.1.1/tests/test_external_specs.py +0 -81
  55. amati-0.1.1/tests/test_logging.py +0 -43
  56. amati-0.1.1/tests/test_references.py +0 -25
  57. {amati-0.1.1 → amati-0.2.1}/.github/workflows/codeql.yml +0 -0
  58. {amati-0.1.1 → amati-0.2.1}/.github/workflows/coverage.yaml +0 -0
  59. {amati-0.1.1 → amati-0.2.1}/.gitignore +0 -0
  60. {amati-0.1.1 → amati-0.2.1}/.pre-commit-config.yaml +0 -0
  61. {amati-0.1.1 → amati-0.2.1}/.pylintrc +0 -0
  62. {amati-0.1.1 → amati-0.2.1}/LICENSE +0 -0
  63. {amati-0.1.1 → amati-0.2.1}/amati/_resolve_forward_references.py +0 -0
  64. {amati-0.1.1 → amati-0.2.1}/amati/data/http-status-codes.json +0 -0
  65. {amati-0.1.1 → amati-0.2.1}/amati/data/iso9110.json +0 -0
  66. {amati-0.1.1 → amati-0.2.1}/amati/data/media-types.json +0 -0
  67. {amati-0.1.1 → amati-0.2.1}/amati/data/schemes.json +0 -0
  68. {amati-0.1.1 → amati-0.2.1}/amati/data/spdx-licences.json +0 -0
  69. {amati-0.1.1 → amati-0.2.1}/amati/data/tlds.json +0 -0
  70. {amati-0.1.1 → amati-0.2.1}/amati/fields/__init__.py +0 -0
  71. {amati-0.1.1 → amati-0.2.1}/amati/fields/_custom_types.py +0 -0
  72. {amati-0.1.1 → amati-0.2.1}/amati/fields/commonmark.py +0 -0
  73. {amati-0.1.1 → amati-0.2.1}/amati/fields/json.py +0 -0
  74. {amati-0.1.1 → amati-0.2.1}/amati/file_handler.py +0 -0
  75. {amati-0.1.1 → amati-0.2.1}/amati/grammars/oas.py +0 -0
  76. {amati-0.1.1 → amati-0.2.1}/amati/grammars/rfc6901.py +0 -0
  77. {amati-0.1.1 → amati-0.2.1}/amati/grammars/rfc7159.py +0 -0
  78. {amati-0.1.1 → amati-0.2.1}/amati/validators/__init__.py +0 -0
  79. {amati-0.1.1 → amati-0.2.1}/bin/startup.sh +0 -0
  80. {amati-0.1.1 → amati-0.2.1}/scripts/data/http_status_code.py +0 -0
  81. {amati-0.1.1 → amati-0.2.1}/scripts/data/iso9110.py +0 -0
  82. {amati-0.1.1 → amati-0.2.1}/scripts/data/media_types.py +0 -0
  83. {amati-0.1.1 → amati-0.2.1}/scripts/data/schemes.py +0 -0
  84. {amati-0.1.1 → amati-0.2.1}/scripts/data/spdx_licences.py +0 -0
  85. {amati-0.1.1 → amati-0.2.1}/scripts/data/tlds.py +0 -0
  86. {amati-0.1.1 → amati-0.2.1}/scripts/tests/setup_test_specs.py +0 -0
  87. {amati-0.1.1 → amati-0.2.1}/tests/__init__.py +0 -0
  88. {amati-0.1.1 → amati-0.2.1}/tests/data/openapi.yaml +0 -0
  89. {amati-0.1.1 → amati-0.2.1}/tests/fields/__init__.py +0 -0
  90. {amati-0.1.1 → amati-0.2.1}/tests/fields/test_iso9110.py +0 -0
  91. {amati-0.1.1 → amati-0.2.1}/tests/fields/test_oas.py +0 -0
  92. {amati-0.1.1 → amati-0.2.1}/tests/fields/test_spdx_licences.py +0 -0
  93. {amati-0.1.1 → amati-0.2.1}/tests/fields/test_uri.py +0 -0
  94. {amati-0.1.1 → amati-0.2.1}/tests/helpers.py +0 -0
  95. {amati-0.1.1 → amati-0.2.1}/tests/test_amati.py +0 -0
  96. {amati-0.1.1 → amati-0.2.1}/tests/validators/__init__.py +0 -0
@@ -0,0 +1 @@
1
+ .venv
@@ -1,6 +1,5 @@
1
1
  version: 2
2
2
  updates:
3
- # Enable version updates for npm
4
3
  - package-ecosystem: "uv"
5
4
  # Look for `uv.lock` file in the root directory.
6
5
  directory: "/"
@@ -8,7 +7,6 @@ updates:
8
7
  schedule:
9
8
  interval: "daily"
10
9
 
11
- # Enable version updates for GitHub Actions
12
10
  - package-ecosystem: "github-actions"
13
11
  # Workflow files stored in the default location of `.github/workflows`
14
12
  # You don't need to specify `/.github/workflows` for `directory`. You can use `directory: "/"`.
@@ -18,3 +16,9 @@ updates:
18
16
  allow:
19
17
  - dependency-type: "direct"
20
18
  - dependency-type: "indirect"
19
+
20
+ - package-ecosystem: "docker"
21
+ # Look for `Dockerfile` in the root directory
22
+ directory: "/"
23
+ schedule:
24
+ interval: "weekly"
@@ -0,0 +1,90 @@
1
+ name: Checks
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [ "main" ]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ build:
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ pull-requests: write
15
+ contents: write
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: dorny/paths-filter@v3
20
+ id: check_changes
21
+ with:
22
+ filters: |
23
+ relevant:
24
+ - '**/*.py'
25
+ - '**/*.sh'
26
+ - '**/*.json'
27
+ - '**/*.html'
28
+ - '**/*.toml'
29
+ - '**/*.lock'
30
+ - 'tests/**/*.yaml'
31
+ - '.pylintrc'
32
+ - '.python-version'
33
+ - '.Dockerfile'
34
+
35
+ - name: Skip message
36
+ if: ${{ !(steps.check_changes.outputs.relevant == 'true') }}
37
+ run: echo "Skipping Python checks - no relevant changes detected"
38
+
39
+
40
+ - name: Install uv
41
+ if: steps.check_changes.outputs.relevant == 'true'
42
+ uses: astral-sh/setup-uv@v6
43
+
44
+ - name: Set up Python
45
+ if: steps.check_changes.outputs.relevant == 'true'
46
+ uses: actions/setup-python@v5
47
+ with:
48
+ python-version-file: ".python-version"
49
+
50
+ - name: Install the project
51
+ if: steps.check_changes.outputs.relevant == 'true'
52
+ run: uv sync --locked --all-extras --dev
53
+
54
+ - name: Formatting
55
+ if: steps.check_changes.outputs.relevant == 'true'
56
+ run: uv run black .
57
+
58
+ - name: Import sorting
59
+ if: steps.check_changes.outputs.relevant == 'true'
60
+ run: uv run isort .
61
+
62
+ - name: Linting
63
+ if: steps.check_changes.outputs.relevant == 'true'
64
+ run: uv run pylint amati
65
+
66
+ - name: Test Linting
67
+ if: steps.check_changes.outputs.relevant == 'true'
68
+ run: uv run pylint tests
69
+
70
+ - name: Testing
71
+ if: steps.check_changes.outputs.relevant == 'true'
72
+ run: uv run pytest -m"not external" --cov
73
+
74
+ - name: Doctests
75
+ if: steps.check_changes.outputs.relevant == 'true'
76
+ run: uv run pytest --doctest-modules amati/
77
+
78
+ - name: Coverage comment
79
+ if: steps.check_changes.outputs.relevant == 'true'
80
+ id: coverage_comment
81
+ uses: py-cov-action/python-coverage-comment-action@v3
82
+ with:
83
+ GITHUB_TOKEN: ${{ github.token }}
84
+
85
+ - name: Store Pull Request comment to be posted
86
+ if: steps.check_changes.outputs.run_python_checks == 'true'
87
+ uses: actions/upload-artifact@v4
88
+ with:
89
+ name: python-coverage-comment-action
90
+ path: python-coverage-comment-action.txt
@@ -0,0 +1,47 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+
8
+ jobs:
9
+ run:
10
+ name: "Build and publish release"
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ id-token: write # Required for OIDC authentication
14
+ contents: read
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v6
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version-file: ".python-version"
24
+ - name: Build
25
+ run: uv build
26
+ - name: Publish to PyPI test
27
+ run: uv publish --index testpypi
28
+ - name: Publish
29
+ run: uv publish
30
+
31
+ - name: Set up Docker Buildx
32
+ uses: docker/setup-buildx-action@v3
33
+
34
+ - name: Log in to Docker Hub
35
+ uses: docker/login-action@v3
36
+ with:
37
+ username: ${{ secrets.DOCKERHUB_USER }}
38
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
39
+
40
+ - name: Build and push Docker image
41
+ uses: docker/build-push-action@v6
42
+ with:
43
+ context: .
44
+ push: true
45
+ tags: |
46
+ ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:${{ github.event.release.tag_name }}-alpha.1
47
+ ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKERHUB_REPO }}:alpha
@@ -0,0 +1 @@
1
+ 3.13.5
amati-0.2.1/Dockerfile ADDED
@@ -0,0 +1,16 @@
1
+ FROM python:3.13-slim
2
+
3
+ ENV PYTHONUNBUFFERED=1
4
+
5
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
6
+
7
+ WORKDIR /app
8
+ COPY pyproject.toml uv.lock README.md ./
9
+ COPY amati/ amati/
10
+
11
+ RUN uv sync --locked --no-dev
12
+
13
+ RUN adduser --disabled-password --gecos '' appuser && chown -R appuser /app
14
+ USER appuser
15
+
16
+ ENTRYPOINT ["uv", "run", "python", "amati/amati.py"]
amati-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.4
2
+ Name: amati
3
+ Version: 0.2.1
4
+ Summary: Validates that a .yaml or .json file conforms to the OpenAPI Specifications 3.x.
5
+ Project-URL: Homepage, https://github.com/ben-alexander/amati
6
+ Project-URL: Issues, https://github.com/ben-alexander/amati/issues
7
+ Author-email: Ben <2551337+ben-alexander@users.noreply.github.com>
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
13
+ Classifier: Topic :: Software Development :: Documentation
14
+ Classifier: Topic :: Software Development :: Testing :: Acceptance
15
+ Requires-Python: >=3.13
16
+ Requires-Dist: abnf>=2.3.1
17
+ Requires-Dist: idna>=3.10
18
+ Requires-Dist: jinja2>=3.1.6
19
+ Requires-Dist: jsonpickle>=4.1.1
20
+ Requires-Dist: jsonschema>=4.24.0
21
+ Requires-Dist: pydantic>=2.11.5
22
+ Requires-Dist: pyyaml>=6.0.2
23
+ Description-Content-Type: text/markdown
24
+
25
+ # amati
26
+
27
+ amati is designed to validate that a file conforms to the [OpenAPI Specification v3.x](https://spec.openapis.org/) (OAS).
28
+
29
+ ## Name
30
+
31
+ "amati" means to observe in Malay, especially with attention to detail. It's also one of the plurals of beloved or favourite in Italian.
32
+
33
+ ## Usage
34
+
35
+ ```sh
36
+ python amati/amati.py --help
37
+ usage: amati [-h] [-s SPEC] [-cc] [-d DISCOVER] [-l] [-hr]
38
+
39
+ Tests whether a OpenAPI specification is valid. Will look an openapi.json or openapi.yaml file in the directory that
40
+ amati is called from. If --discover is set will search the directory tree. If the specification does not follow the
41
+ naming recommendation the --spec switch should be used. Creates a file <filename>.errors.json alongside the original
42
+ specification containing a JSON representation of all the errors.
43
+
44
+ options:
45
+ -h, --help show this help message and exit
46
+ -s, --spec SPEC The specification to be parsed
47
+ -cc, --consistency-check
48
+ Runs a consistency check between the input specification and the parsed specification
49
+ -d, --discover DISCOVER
50
+ Searches the specified directory tree for openapi.yaml or openapi.json.
51
+ -l, --local Store errors local to the caller in a file called <file-name>.errors.json; a .amati/ directory
52
+ will be created.
53
+ -hr, --html-report Creates an HTML report of the errors, called <file-name>.errors.html, alongside the original
54
+ file or in a .amati/ directory if the --local switch is used
55
+ ```
56
+
57
+ A Dockerfile is available on [DockerHub](https://hub.docker.com/r/benale/amati/tags)
58
+
59
+ To run against a specific specification the location of the specification needs to be mounted in the container.
60
+
61
+ ```sh
62
+ docker run -v "<path-to-mount>:/<mount-name> amati:alpha <options>
63
+ ```
64
+
65
+ e.g.
66
+
67
+ ```sh
68
+ docker run -v /Users/myuser/myrepo:/data amati:alpha --spec data/myspec.yaml --hr
69
+ ```
70
+
71
+ ## Architecture
72
+
73
+ amati uses Pydantic, especially the validation, and Typing to construct the entire OAS as a single data type. Passing a dictionary to the top-level data type runs all the validation in the Pydantic models constructing a single set of inherited classes and datatypes that validate that the API specification is accurate. To the extent that Pydantic is functional, amati has a [functional core and an imperative shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell).
74
+
75
+ Where the specification conforms, but relies on implementation-defined behavior (e.g. [data type formats](https://spec.openapis.org/oas/v3.1.1.html#data-type-format)), a warning will be raised.
76
+
77
+ ## Contributing
78
+
79
+ ### Prerequisites
80
+
81
+ * The latest version of [uv](https://docs.astral.sh/uv/)
82
+ * [git 2.49+](https://git-scm.com/downloads/linux)
83
+ * [Docker](https://docs.docker.com/engine/install/)
84
+
85
+ ### Starting
86
+
87
+ The project uses a [`pyproject.toml` file](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#writing-pyproject-toml) to determine what to build.
88
+
89
+ To get started run:
90
+
91
+ ```sh
92
+ uv python install
93
+ uv venv
94
+ uv sync
95
+ ```
96
+
97
+ ### Testing and formatting
98
+
99
+ This project uses:
100
+
101
+ * [Pytest](https://docs.pytest.org/en/stable/) as a testing framework
102
+ * [Pyright](https://microsoft.github.io/pyright/#/) on strict mode for type checking
103
+ * [Pylint](https://www.pylint.org/) as a linter, using a modified version from [Google's style guide](https://google.github.io/styleguide/pyguide.html)
104
+ * [Hypothesis](https://hypothesis.readthedocs.io/en/latest/index.html) for test data generation
105
+ * [Coverage](https://coverage.readthedocs.io/en/7.6.8/) on both the tests and code for test coverage
106
+ * [Black](https://black.readthedocs.io/en/stable/index.html) for automated formatting
107
+ * [isort](https://pycqa.github.io/isort/) for import sorting
108
+
109
+ It's expected that there are no errors and 100% of the code is reached and executed. The strategy for test coverage is based on parsing test specifications and not unit tests.
110
+ amati runs tests on the external specifications, detailed in `tests/data/.amati.tests.yaml`. To be able to run these tests the GitHub repos containing the specifications need to be available locally. Specific revisions of the repos can be downloaded by running the following, which will clone the repos into `../amati-tests-specs/<repo-name>`.
111
+
112
+ ```sh
113
+ python scripts/tests/setup_test_specs.py
114
+ ```
115
+
116
+ If there are some issues with the specification a JSON file detailing those should be placed into `tests/data/` and the name of that file noted in `tests/data/.amati.tests.yaml` for the test suite to pick it up and check that the errors are expected. Any specifications that close the coverage gap are gratefully received.
117
+
118
+ To run everything, from linting, type checking to downloading test specs and building and testing the Docker image run:
119
+
120
+ ```sh
121
+ sh bin/checks.sh
122
+ ```
123
+
124
+ ### Docker
125
+
126
+ A development Docker image is provided, `Dockerfile.dev`, to build:
127
+
128
+ ```sh
129
+ docker build -t amati -f Dockerfile .
130
+ ```
131
+
132
+ to run against a specific specification the location of the specification needs to be mounted in the container.
133
+
134
+ ```sh
135
+ docker run -v "<path-to-mount>:/<mount-name> amati <options>
136
+ ```
137
+
138
+ This can be tested against a provided specification, from the root directory
139
+
140
+ ```sh
141
+ docker run --detach -v "$(pwd):/data" amati <options>
142
+ ```
143
+
144
+
145
+ ### Data
146
+
147
+ There are some scripts to create the data needed by the project, for example, all the registered TLDs. To refresh the data, run the contents of `/scripts/data`.
148
+
149
+
150
+
151
+
amati-0.2.1/README.md ADDED
@@ -0,0 +1,127 @@
1
+ # amati
2
+
3
+ amati is designed to validate that a file conforms to the [OpenAPI Specification v3.x](https://spec.openapis.org/) (OAS).
4
+
5
+ ## Name
6
+
7
+ "amati" means to observe in Malay, especially with attention to detail. It's also one of the plurals of beloved or favourite in Italian.
8
+
9
+ ## Usage
10
+
11
+ ```sh
12
+ python amati/amati.py --help
13
+ usage: amati [-h] [-s SPEC] [-cc] [-d DISCOVER] [-l] [-hr]
14
+
15
+ Tests whether a OpenAPI specification is valid. Will look an openapi.json or openapi.yaml file in the directory that
16
+ amati is called from. If --discover is set will search the directory tree. If the specification does not follow the
17
+ naming recommendation the --spec switch should be used. Creates a file <filename>.errors.json alongside the original
18
+ specification containing a JSON representation of all the errors.
19
+
20
+ options:
21
+ -h, --help show this help message and exit
22
+ -s, --spec SPEC The specification to be parsed
23
+ -cc, --consistency-check
24
+ Runs a consistency check between the input specification and the parsed specification
25
+ -d, --discover DISCOVER
26
+ Searches the specified directory tree for openapi.yaml or openapi.json.
27
+ -l, --local Store errors local to the caller in a file called <file-name>.errors.json; a .amati/ directory
28
+ will be created.
29
+ -hr, --html-report Creates an HTML report of the errors, called <file-name>.errors.html, alongside the original
30
+ file or in a .amati/ directory if the --local switch is used
31
+ ```
32
+
33
+ A Dockerfile is available on [DockerHub](https://hub.docker.com/r/benale/amati/tags)
34
+
35
+ To run against a specific specification the location of the specification needs to be mounted in the container.
36
+
37
+ ```sh
38
+ docker run -v "<path-to-mount>:/<mount-name> amati:alpha <options>
39
+ ```
40
+
41
+ e.g.
42
+
43
+ ```sh
44
+ docker run -v /Users/myuser/myrepo:/data amati:alpha --spec data/myspec.yaml --hr
45
+ ```
46
+
47
+ ## Architecture
48
+
49
+ amati uses Pydantic, especially the validation, and Typing to construct the entire OAS as a single data type. Passing a dictionary to the top-level data type runs all the validation in the Pydantic models constructing a single set of inherited classes and datatypes that validate that the API specification is accurate. To the extent that Pydantic is functional, amati has a [functional core and an imperative shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell).
50
+
51
+ Where the specification conforms, but relies on implementation-defined behavior (e.g. [data type formats](https://spec.openapis.org/oas/v3.1.1.html#data-type-format)), a warning will be raised.
52
+
53
+ ## Contributing
54
+
55
+ ### Prerequisites
56
+
57
+ * The latest version of [uv](https://docs.astral.sh/uv/)
58
+ * [git 2.49+](https://git-scm.com/downloads/linux)
59
+ * [Docker](https://docs.docker.com/engine/install/)
60
+
61
+ ### Starting
62
+
63
+ The project uses a [`pyproject.toml` file](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#writing-pyproject-toml) to determine what to build.
64
+
65
+ To get started run:
66
+
67
+ ```sh
68
+ uv python install
69
+ uv venv
70
+ uv sync
71
+ ```
72
+
73
+ ### Testing and formatting
74
+
75
+ This project uses:
76
+
77
+ * [Pytest](https://docs.pytest.org/en/stable/) as a testing framework
78
+ * [Pyright](https://microsoft.github.io/pyright/#/) on strict mode for type checking
79
+ * [Pylint](https://www.pylint.org/) as a linter, using a modified version from [Google's style guide](https://google.github.io/styleguide/pyguide.html)
80
+ * [Hypothesis](https://hypothesis.readthedocs.io/en/latest/index.html) for test data generation
81
+ * [Coverage](https://coverage.readthedocs.io/en/7.6.8/) on both the tests and code for test coverage
82
+ * [Black](https://black.readthedocs.io/en/stable/index.html) for automated formatting
83
+ * [isort](https://pycqa.github.io/isort/) for import sorting
84
+
85
+ It's expected that there are no errors and 100% of the code is reached and executed. The strategy for test coverage is based on parsing test specifications and not unit tests.
86
+ amati runs tests on the external specifications, detailed in `tests/data/.amati.tests.yaml`. To be able to run these tests the GitHub repos containing the specifications need to be available locally. Specific revisions of the repos can be downloaded by running the following, which will clone the repos into `../amati-tests-specs/<repo-name>`.
87
+
88
+ ```sh
89
+ python scripts/tests/setup_test_specs.py
90
+ ```
91
+
92
+ If there are some issues with the specification a JSON file detailing those should be placed into `tests/data/` and the name of that file noted in `tests/data/.amati.tests.yaml` for the test suite to pick it up and check that the errors are expected. Any specifications that close the coverage gap are gratefully received.
93
+
94
+ To run everything, from linting, type checking to downloading test specs and building and testing the Docker image run:
95
+
96
+ ```sh
97
+ sh bin/checks.sh
98
+ ```
99
+
100
+ ### Docker
101
+
102
+ A development Docker image is provided, `Dockerfile.dev`, to build:
103
+
104
+ ```sh
105
+ docker build -t amati -f Dockerfile .
106
+ ```
107
+
108
+ to run against a specific specification the location of the specification needs to be mounted in the container.
109
+
110
+ ```sh
111
+ docker run -v "<path-to-mount>:/<mount-name> amati <options>
112
+ ```
113
+
114
+ This can be tested against a provided specification, from the root directory
115
+
116
+ ```sh
117
+ docker run --detach -v "$(pwd):/data" amati <options>
118
+ ```
119
+
120
+
121
+ ### Data
122
+
123
+ There are some scripts to create the data needed by the project, for example, all the registered TLDs. To refresh the data, run the contents of `/scripts/data`.
124
+
125
+
126
+
127
+
@@ -0,0 +1,116 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>amati</title>
7
+ <!-- Tailwind CSS CDN -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <style>
10
+ body {
11
+ font-family: "Inter", sans-serif;
12
+ background-color: #f3f4f6;
13
+ display: flex;
14
+ justify-content: center;
15
+ align-items: flex-start;
16
+ min-height: 100vh;
17
+ padding: 2rem;
18
+ box-sizing: border-box;
19
+ }
20
+ .container {
21
+ background-color: #ffffff;
22
+ padding: 2rem;
23
+ border-radius: 1rem;
24
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
25
+ width: 100%;
26
+ max-width: 1200px; /* Increased max-width for more columns */
27
+ overflow-x: auto; /* Added for horizontal scrolling on small screens */
28
+ }
29
+ table {
30
+ width: 100%;
31
+ border-collapse: collapse;
32
+ }
33
+ th, td {
34
+ padding: 0.75rem;
35
+ text-align: left;
36
+ border-bottom: 1px solid #e5e7eb;
37
+ }
38
+ th {
39
+ background-color: #f9fafb;
40
+ font-weight: 600;
41
+ color: #4b5563;
42
+ text-transform: uppercase;
43
+ font-size: 0.875rem;
44
+ }
45
+ tr:hover {
46
+ background-color: #f3f4f6;
47
+ }
48
+ .code-block {
49
+ background-color: #e0e7ff;
50
+ padding: 0.25rem 0.5rem;
51
+ border-radius: 0.375rem;
52
+ font-family: monospace;
53
+ color: #4f46e5;
54
+ white-space: nowrap; /* Prevent line breaks for code */
55
+ }
56
+ .error-url {
57
+ color: #3b82f6;
58
+ text-decoration: none;
59
+ }
60
+ .error-url:hover {
61
+ text-decoration: underline;
62
+ }
63
+ </style>
64
+ </head>
65
+ <body>
66
+ <div class="container">
67
+ <h1 class="text-3xl font-bold text-center text-gray-800 mb-6">amati error report</h1>
68
+
69
+ {% if errors %}
70
+ <div class="overflow-x-auto rounded-lg shadow-sm border border-gray-200">
71
+ <table class="min-w-full divide-y divide-gray-200">
72
+ <thead class="bg-gray-50">
73
+ <tr>
74
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider rounded-tl-lg">Type</th>
75
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Location</th>
76
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Message</th>
77
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Input</th>
78
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider rounded-tr-lg">URL</th>
79
+ </tr>
80
+ </thead>
81
+ <tbody class="bg-white divide-y divide-gray-200">
82
+ {% for item in errors %}
83
+ <tr>
84
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">{{ item.type | default('N/A') }}</td>
85
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
86
+ <code class="code-block">
87
+ {# Check if 'loc' exists and is iterable before joining #}
88
+ {% if item.loc is defined and item.loc is iterable %}
89
+ {{ item.loc | join(' -> ') }}
90
+ {% else %}
91
+ N/A
92
+ {% endif %}
93
+ </code>
94
+ </td>
95
+ <td class="px-6 py-4 text-sm text-gray-500">{{ item.msg | default('N/A') }}</td>
96
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
97
+ <code class="code-block">
98
+ {% if item.input is defined and item.input %}
99
+ {{ item.input | default('N/A') }}
100
+ {% else %}
101
+ N/A
102
+ {% endif %}
103
+ </code>
104
+ </td>
105
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-blue-600"><a href="{{ item.url | default('#') }}" class="error-url" target="_blank">{{ item.url | default('N/A') }}</a></td>
106
+ </tr>
107
+ {% endfor %}
108
+ </tbody>
109
+ </table>
110
+ </div>
111
+ {% else %}
112
+ <p class="text-center text-gray-600 mt-8">No data available to display.</p>
113
+ {% endif %}
114
+ </div>
115
+ </body>
116
+ </html>
@@ -13,4 +13,3 @@ __version__ = importlib.metadata.version("amati")
13
13
 
14
14
  from amati.amati import dispatch, run
15
15
  from amati.exceptions import AmatiValueError
16
- from amati.references import AmatiReferenceException, Reference, References
@@ -0,0 +1,48 @@
1
+ """
2
+ Handles Pydantic errors and amati logs to provide a consistent view to the user.
3
+ """
4
+
5
+ import json
6
+ from typing import cast
7
+
8
+ from amati.logging import Log
9
+
10
+ type JSONPrimitive = str | int | float | bool | None
11
+ type JSONArray = list["JSONValue"]
12
+ type JSONObject = dict[str, "JSONValue"]
13
+ type JSONValue = JSONPrimitive | JSONArray | JSONObject
14
+
15
+
16
+ def remove_duplicates(data: list[JSONObject]) -> list[JSONObject]:
17
+ """
18
+ Remove duplicates by converting each dict to a JSON string for comparison.
19
+ """
20
+ seen: set[str] = set()
21
+ unique_data: list[JSONObject] = []
22
+
23
+ for item in data:
24
+ # Convert to JSON string with sorted keys for consistent hashing
25
+ item_json = json.dumps(item, sort_keys=True, separators=(",", ":"))
26
+ if item_json not in seen:
27
+ seen.add(item_json)
28
+ unique_data.append(item)
29
+
30
+ return unique_data
31
+
32
+
33
+ def handle_errors(errors: list[JSONObject] | None, logs: list[Log]) -> list[JSONObject]:
34
+ """
35
+ Makes errors and logs consistent for user consumption.
36
+ """
37
+
38
+ result: list[JSONObject] = []
39
+
40
+ if errors:
41
+ result.extend(errors)
42
+
43
+ if logs:
44
+ result.extend(cast(list[JSONObject], logs))
45
+
46
+ result = remove_duplicates(result)
47
+
48
+ return result