amati 0.1.1__tar.gz → 0.2__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.
- amati-0.2/.dockerignore +1 -0
- amati-0.2/Dockerfile +16 -0
- {amati-0.1.1 → amati-0.2}/PKG-INFO +71 -9
- amati-0.2/README.md +127 -0
- amati-0.2/TEMPLATE.html +116 -0
- {amati-0.1.1 → amati-0.2}/amati/__init__.py +0 -1
- amati-0.2/amati/_error_handler.py +48 -0
- {amati-0.1.1 → amati-0.2}/amati/amati.py +86 -20
- {amati-0.1.1 → amati-0.2}/amati/exceptions.py +2 -4
- {amati-0.1.1 → amati-0.2}/amati/fields/email.py +3 -7
- {amati-0.1.1 → amati-0.2}/amati/fields/http_status_codes.py +4 -5
- {amati-0.1.1 → amati-0.2}/amati/fields/iso9110.py +4 -5
- {amati-0.1.1 → amati-0.2}/amati/fields/media.py +3 -7
- {amati-0.1.1 → amati-0.2}/amati/fields/oas.py +6 -12
- {amati-0.1.1 → amati-0.2}/amati/fields/spdx_licences.py +4 -7
- {amati-0.1.1 → amati-0.2}/amati/fields/uri.py +3 -11
- {amati-0.1.1 → amati-0.2}/amati/logging.py +8 -8
- {amati-0.1.1 → amati-0.2}/amati/model_validators.py +42 -33
- {amati-0.1.1 → amati-0.2}/amati/validators/generic.py +18 -13
- {amati-0.1.1 → amati-0.2}/amati/validators/oas304.py +93 -130
- {amati-0.1.1 → amati-0.2}/amati/validators/oas311.py +58 -156
- {amati-0.1.1 → amati-0.2}/bin/checks.sh +5 -1
- {amati-0.1.1 → amati-0.2}/pyproject.toml +2 -1
- {amati-0.1.1 → amati-0.2}/tests/data/.amati.tests.yaml +22 -11
- amati-0.1.1/tests/data/DigitalOcean-public.v2.errors → amati-0.2/tests/data/DigitalOcean-public.v2.errors.json +0 -13
- amati-0.2/tests/data/api.github.com.yaml.errors.json +23 -0
- amati-0.2/tests/data/next-api.github.com.yaml.errors.json +32 -0
- amati-0.2/tests/data/redocly.openapi.yaml.errors.json +23 -0
- {amati-0.1.1 → amati-0.2}/tests/fields/test_email.py +1 -2
- {amati-0.1.1 → amati-0.2}/tests/fields/test_http_status_codes.py +6 -1
- {amati-0.1.1 → amati-0.2}/tests/fields/test_media.py +1 -1
- {amati-0.1.1 → amati-0.2}/tests/model_validators/test_all_of.py +3 -4
- {amati-0.1.1 → amati-0.2}/tests/model_validators/test_at_least_one.py +3 -4
- {amati-0.1.1 → amati-0.2}/tests/model_validators/test_if_then.py +18 -17
- {amati-0.1.1 → amati-0.2}/tests/model_validators/test_only_one.py +3 -4
- amati-0.2/tests/test_external_specs.py +93 -0
- amati-0.2/tests/test_logging.py +31 -0
- {amati-0.1.1 → amati-0.2}/tests/validators/test_generic.py +6 -4
- {amati-0.1.1 → amati-0.2}/tests/validators/test_licence_object.py +10 -10
- {amati-0.1.1 → amati-0.2}/tests/validators/test_security_scheme_object.py +4 -4
- {amati-0.1.1 → amati-0.2}/tests/validators/test_server_variable_object.py +2 -2
- {amati-0.1.1 → amati-0.2}/uv.lock +43 -1
- amati-0.1.1/README.md +0 -66
- amati-0.1.1/amati/references.py +0 -33
- amati-0.1.1/tests/test_external_specs.py +0 -81
- amati-0.1.1/tests/test_logging.py +0 -43
- amati-0.1.1/tests/test_references.py +0 -25
- {amati-0.1.1 → amati-0.2}/.github/dependabot.yml +0 -0
- {amati-0.1.1 → amati-0.2}/.github/workflows/checks.yaml +0 -0
- {amati-0.1.1 → amati-0.2}/.github/workflows/codeql.yml +0 -0
- {amati-0.1.1 → amati-0.2}/.github/workflows/coverage.yaml +0 -0
- {amati-0.1.1 → amati-0.2}/.github/workflows/publish.yaml +0 -0
- {amati-0.1.1 → amati-0.2}/.gitignore +0 -0
- {amati-0.1.1 → amati-0.2}/.pre-commit-config.yaml +0 -0
- {amati-0.1.1 → amati-0.2}/.pylintrc +0 -0
- {amati-0.1.1 → amati-0.2}/.python-version +0 -0
- {amati-0.1.1 → amati-0.2}/LICENSE +0 -0
- {amati-0.1.1 → amati-0.2}/amati/_resolve_forward_references.py +0 -0
- {amati-0.1.1 → amati-0.2}/amati/data/http-status-codes.json +0 -0
- {amati-0.1.1 → amati-0.2}/amati/data/iso9110.json +0 -0
- {amati-0.1.1 → amati-0.2}/amati/data/media-types.json +0 -0
- {amati-0.1.1 → amati-0.2}/amati/data/schemes.json +0 -0
- {amati-0.1.1 → amati-0.2}/amati/data/spdx-licences.json +0 -0
- {amati-0.1.1 → amati-0.2}/amati/data/tlds.json +0 -0
- {amati-0.1.1 → amati-0.2}/amati/fields/__init__.py +0 -0
- {amati-0.1.1 → amati-0.2}/amati/fields/_custom_types.py +0 -0
- {amati-0.1.1 → amati-0.2}/amati/fields/commonmark.py +0 -0
- {amati-0.1.1 → amati-0.2}/amati/fields/json.py +0 -0
- {amati-0.1.1 → amati-0.2}/amati/file_handler.py +0 -0
- {amati-0.1.1 → amati-0.2}/amati/grammars/oas.py +0 -0
- {amati-0.1.1 → amati-0.2}/amati/grammars/rfc6901.py +0 -0
- {amati-0.1.1 → amati-0.2}/amati/grammars/rfc7159.py +0 -0
- {amati-0.1.1 → amati-0.2}/amati/validators/__init__.py +0 -0
- {amati-0.1.1 → amati-0.2}/bin/startup.sh +0 -0
- {amati-0.1.1 → amati-0.2}/scripts/data/http_status_code.py +0 -0
- {amati-0.1.1 → amati-0.2}/scripts/data/iso9110.py +0 -0
- {amati-0.1.1 → amati-0.2}/scripts/data/media_types.py +0 -0
- {amati-0.1.1 → amati-0.2}/scripts/data/schemes.py +0 -0
- {amati-0.1.1 → amati-0.2}/scripts/data/spdx_licences.py +0 -0
- {amati-0.1.1 → amati-0.2}/scripts/data/tlds.py +0 -0
- {amati-0.1.1 → amati-0.2}/scripts/tests/setup_test_specs.py +0 -0
- {amati-0.1.1 → amati-0.2}/tests/__init__.py +0 -0
- {amati-0.1.1 → amati-0.2}/tests/data/openapi.yaml +0 -0
- {amati-0.1.1 → amati-0.2}/tests/fields/__init__.py +0 -0
- {amati-0.1.1 → amati-0.2}/tests/fields/test_iso9110.py +0 -0
- {amati-0.1.1 → amati-0.2}/tests/fields/test_oas.py +0 -0
- {amati-0.1.1 → amati-0.2}/tests/fields/test_spdx_licences.py +0 -0
- {amati-0.1.1 → amati-0.2}/tests/fields/test_uri.py +0 -0
- {amati-0.1.1 → amati-0.2}/tests/helpers.py +0 -0
- {amati-0.1.1 → amati-0.2}/tests/test_amati.py +0 -0
- {amati-0.1.1 → amati-0.2}/tests/validators/__init__.py +0 -0
amati-0.2/.dockerignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
.venv
|
amati-0.2/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"]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: amati
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.2
|
4
4
|
Summary: Validates that a .yaml or .json file conforms to the OpenAPI Specifications 3.x.
|
5
5
|
Project-URL: Homepage, https://github.com/ben-alexander/amati
|
6
6
|
Project-URL: Issues, https://github.com/ben-alexander/amati/issues
|
@@ -15,6 +15,7 @@ Classifier: Topic :: Software Development :: Testing :: Acceptance
|
|
15
15
|
Requires-Python: >=3.13
|
16
16
|
Requires-Dist: abnf>=2.3.1
|
17
17
|
Requires-Dist: idna>=3.10
|
18
|
+
Requires-Dist: jinja2>=3.1.6
|
18
19
|
Requires-Dist: jsonpickle>=4.1.1
|
19
20
|
Requires-Dist: jsonschema>=4.24.0
|
20
21
|
Requires-Dist: pydantic>=2.11.5
|
@@ -23,26 +24,64 @@ Description-Content-Type: text/markdown
|
|
23
24
|
|
24
25
|
# amati
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
Currently a proof of concept.
|
27
|
+
amati is designed to validate that a file conforms to the [OpenAPI Specification v3.x](https://spec.openapis.org/) (OAS).
|
29
28
|
|
30
29
|
## Name
|
31
30
|
|
32
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.
|
33
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 <options>
|
63
|
+
```
|
64
|
+
|
65
|
+
e.g.
|
66
|
+
|
67
|
+
```sh
|
68
|
+
docker run -v /Users/myuser/myrepo:/data amati --spec data/myspec.yaml --hr
|
69
|
+
```
|
70
|
+
|
34
71
|
## Architecture
|
35
72
|
|
36
73
|
This 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.
|
37
74
|
|
38
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.
|
39
76
|
|
40
|
-
##
|
77
|
+
## Contributing
|
78
|
+
|
79
|
+
### Requirements
|
41
80
|
|
42
81
|
* The latest version of [uv](https://docs.astral.sh/uv/)
|
43
82
|
* [git 2.49+](https://git-scm.com/downloads/linux)
|
44
83
|
|
45
|
-
|
84
|
+
### Testing and formatting
|
46
85
|
|
47
86
|
This project uses:
|
48
87
|
|
@@ -54,7 +93,7 @@ This project uses:
|
|
54
93
|
* [Black](https://black.readthedocs.io/en/stable/index.html) for automated formatting
|
55
94
|
* [isort](https://pycqa.github.io/isort/) for import sorting
|
56
95
|
|
57
|
-
It's expected that there are no errors
|
96
|
+
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.
|
58
97
|
|
59
98
|
amati runs tests on external specifications, detailed in `tests/data/.amati.tests.yaml`. To be able to run these tests the appropriate GitHub repos need to be local. Specific revisions of the repos can be downloaded by running
|
60
99
|
|
@@ -62,13 +101,15 @@ amati runs tests on external specifications, detailed in `tests/data/.amati.test
|
|
62
101
|
python scripts/tests/setup_test_specs.py
|
63
102
|
```
|
64
103
|
|
65
|
-
To run everything, from linting, type checking to downloading test specs run:
|
104
|
+
To run everything, from linting, type checking to downloading test specs and building and testing the Docker image run:
|
66
105
|
|
67
106
|
```sh
|
68
107
|
sh bin/checks.sh
|
69
108
|
```
|
70
109
|
|
71
|
-
|
110
|
+
You will need to have Docker installed.
|
111
|
+
|
112
|
+
### Building
|
72
113
|
|
73
114
|
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.
|
74
115
|
|
@@ -80,6 +121,27 @@ uv venv
|
|
80
121
|
uv sync
|
81
122
|
```
|
82
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
|
+
and 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
|
142
|
+
```
|
143
|
+
|
144
|
+
|
83
145
|
### Data
|
84
146
|
|
85
147
|
There are some scripts to create the data needed by the project, for example, all the possible licences. If the data needs to be refreshed this can be done by running the contents of `/scripts/data`.
|
amati-0.2/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 <options>
|
39
|
+
```
|
40
|
+
|
41
|
+
e.g.
|
42
|
+
|
43
|
+
```sh
|
44
|
+
docker run -v /Users/myuser/myrepo:/data amati --spec data/myspec.yaml --hr
|
45
|
+
```
|
46
|
+
|
47
|
+
## Architecture
|
48
|
+
|
49
|
+
This 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.
|
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
|
+
### Requirements
|
56
|
+
|
57
|
+
* The latest version of [uv](https://docs.astral.sh/uv/)
|
58
|
+
* [git 2.49+](https://git-scm.com/downloads/linux)
|
59
|
+
|
60
|
+
### Testing and formatting
|
61
|
+
|
62
|
+
This project uses:
|
63
|
+
|
64
|
+
* [Pytest](https://docs.pytest.org/en/stable/) as a testing framework
|
65
|
+
* [PyLance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) on strict mode for type checking
|
66
|
+
* [Pylint](https://www.pylint.org/) as a linter, using a modified version from [Google's style guide](https://google.github.io/styleguide/pyguide.html)
|
67
|
+
* [Hypothesis](https://hypothesis.readthedocs.io/en/latest/index.html) for test data generation
|
68
|
+
* [Coverage](https://coverage.readthedocs.io/en/7.6.8/) on both the tests and code for test coverage
|
69
|
+
* [Black](https://black.readthedocs.io/en/stable/index.html) for automated formatting
|
70
|
+
* [isort](https://pycqa.github.io/isort/) for import sorting
|
71
|
+
|
72
|
+
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.
|
73
|
+
|
74
|
+
amati runs tests on external specifications, detailed in `tests/data/.amati.tests.yaml`. To be able to run these tests the appropriate GitHub repos need to be local. Specific revisions of the repos can be downloaded by running
|
75
|
+
|
76
|
+
```sh
|
77
|
+
python scripts/tests/setup_test_specs.py
|
78
|
+
```
|
79
|
+
|
80
|
+
To run everything, from linting, type checking to downloading test specs and building and testing the Docker image run:
|
81
|
+
|
82
|
+
```sh
|
83
|
+
sh bin/checks.sh
|
84
|
+
```
|
85
|
+
|
86
|
+
You will need to have Docker installed.
|
87
|
+
|
88
|
+
### Building
|
89
|
+
|
90
|
+
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.
|
91
|
+
|
92
|
+
To install, assuming that [uv](https://docs.astral.sh/uv/) is already installed and initialised
|
93
|
+
|
94
|
+
```sh
|
95
|
+
uv python install
|
96
|
+
uv venv
|
97
|
+
uv sync
|
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
|
+
and 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
|
118
|
+
```
|
119
|
+
|
120
|
+
|
121
|
+
### Data
|
122
|
+
|
123
|
+
There are some scripts to create the data needed by the project, for example, all the possible licences. If the data needs to be refreshed this can be done by running the contents of `/scripts/data`.
|
124
|
+
|
125
|
+
|
126
|
+
|
127
|
+
|
amati-0.2/TEMPLATE.html
ADDED
@@ -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>
|
@@ -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
|
@@ -7,15 +7,16 @@ import json
|
|
7
7
|
import sys
|
8
8
|
from pathlib import Path
|
9
9
|
|
10
|
-
import
|
10
|
+
from jinja2 import Environment, FileSystemLoader
|
11
11
|
from pydantic import BaseModel, ValidationError
|
12
|
-
from pydantic_core import ErrorDetails
|
13
12
|
|
14
13
|
# pylint: disable=wrong-import-position
|
15
14
|
|
16
15
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
16
|
+
from amati._error_handler import handle_errors
|
17
17
|
from amati._resolve_forward_references import resolve_forward_references
|
18
18
|
from amati.file_handler import load_file
|
19
|
+
from amati.logging import Log, LogMixin
|
19
20
|
|
20
21
|
type JSONPrimitive = str | int | float | bool | None
|
21
22
|
type JSONArray = list["JSONValue"]
|
@@ -23,7 +24,7 @@ type JSONObject = dict[str, "JSONValue"]
|
|
23
24
|
type JSONValue = JSONPrimitive | JSONArray | JSONObject
|
24
25
|
|
25
26
|
|
26
|
-
def dispatch(data: JSONObject) -> tuple[BaseModel | None, list[
|
27
|
+
def dispatch(data: JSONObject) -> tuple[BaseModel | None, list[JSONObject] | None]:
|
27
28
|
"""
|
28
29
|
Returns the correct model for the passed spec
|
29
30
|
|
@@ -59,7 +60,7 @@ def dispatch(data: JSONObject) -> tuple[BaseModel | None, list[ErrorDetails] | N
|
|
59
60
|
try:
|
60
61
|
model = module.OpenAPIObject(**data)
|
61
62
|
except ValidationError as e:
|
62
|
-
return None, e.
|
63
|
+
return None, json.loads(e.json())
|
63
64
|
|
64
65
|
return model, None
|
65
66
|
|
@@ -86,7 +87,12 @@ def check(original: JSONObject, validated: BaseModel) -> bool:
|
|
86
87
|
return original_ == new_
|
87
88
|
|
88
89
|
|
89
|
-
def run(
|
90
|
+
def run(
|
91
|
+
file_path: str | Path,
|
92
|
+
consistency_check: bool = False,
|
93
|
+
local: bool = False,
|
94
|
+
html_report: bool = False,
|
95
|
+
):
|
90
96
|
"""
|
91
97
|
Runs the full amati process on a specific specification file.
|
92
98
|
|
@@ -101,24 +107,56 @@ def run(file_path: str | Path, consistency_check: bool = False):
|
|
101
107
|
consistency_check: Whether or not to verify the output against the input
|
102
108
|
"""
|
103
109
|
|
104
|
-
|
110
|
+
spec = Path(file_path)
|
105
111
|
|
106
|
-
|
112
|
+
data = load_file(spec)
|
107
113
|
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
114
|
+
logs: list[Log] = []
|
115
|
+
|
116
|
+
with LogMixin.context():
|
117
|
+
result, errors = dispatch(data)
|
118
|
+
logs.extend(LogMixin.logs)
|
119
|
+
|
120
|
+
if errors or logs:
|
121
|
+
|
122
|
+
handled_errors: list[JSONObject] = handle_errors(errors, logs)
|
123
|
+
|
124
|
+
file_name = Path(Path(file_path).parts[-1])
|
125
|
+
error_file = file_name.with_suffix(file_name.suffix + ".errors")
|
126
|
+
error_path = spec.parent
|
113
127
|
|
114
|
-
|
115
|
-
|
116
|
-
Path(".amati").mkdir()
|
128
|
+
if local:
|
129
|
+
error_path = Path(".amati")
|
117
130
|
|
118
|
-
|
131
|
+
if not error_path.exists():
|
132
|
+
error_path.mkdir()
|
119
133
|
|
120
|
-
with open(
|
121
|
-
|
134
|
+
with open(
|
135
|
+
error_path / error_file.with_suffix(error_file.suffix + ".json"),
|
136
|
+
"w",
|
137
|
+
encoding="utf-8",
|
138
|
+
) as f:
|
139
|
+
f.write(json.dumps(handled_errors))
|
140
|
+
|
141
|
+
if html_report:
|
142
|
+
env = Environment(
|
143
|
+
loader=FileSystemLoader(".")
|
144
|
+
) # Assumes template is in the same directory
|
145
|
+
template = env.get_template("TEMPLATE.html")
|
146
|
+
|
147
|
+
# Render the template with your data
|
148
|
+
html_output = template.render(errors=handled_errors)
|
149
|
+
|
150
|
+
# Save the output to a file
|
151
|
+
with open(
|
152
|
+
error_path / error_file.with_suffix(error_file.suffix + ".html"),
|
153
|
+
"w",
|
154
|
+
encoding="utf-8",
|
155
|
+
) as f:
|
156
|
+
f.write(html_output)
|
157
|
+
|
158
|
+
if result and consistency_check:
|
159
|
+
return check(data, result)
|
122
160
|
|
123
161
|
|
124
162
|
def discover(discover_dir: str = ".") -> list[Path]:
|
@@ -170,6 +208,9 @@ if __name__ == "__main__":
|
|
170
208
|
--discover is set will search the directory tree. If the specification
|
171
209
|
does not follow the naming recommendation the --spec switch should be
|
172
210
|
used.
|
211
|
+
|
212
|
+
Creates a file <filename>.errors.json alongside the original specification
|
213
|
+
containing a JSON representation of all the errors.
|
173
214
|
""",
|
174
215
|
)
|
175
216
|
|
@@ -185,7 +226,8 @@ if __name__ == "__main__":
|
|
185
226
|
"--consistency-check",
|
186
227
|
required=False,
|
187
228
|
action="store_true",
|
188
|
-
help="Runs a consistency check between the input specification and
|
229
|
+
help="Runs a consistency check between the input specification and the"
|
230
|
+
" parsed specification",
|
189
231
|
)
|
190
232
|
|
191
233
|
parser.add_argument(
|
@@ -196,6 +238,25 @@ if __name__ == "__main__":
|
|
196
238
|
help="Searches the specified directory tree for openapi.yaml or openapi.json.",
|
197
239
|
)
|
198
240
|
|
241
|
+
parser.add_argument(
|
242
|
+
"-l",
|
243
|
+
"--local",
|
244
|
+
required=False,
|
245
|
+
action="store_true",
|
246
|
+
help="Store errors local to the caller in a file called <file-name>.errors.json"
|
247
|
+
"; a .amati/ directory will be created.",
|
248
|
+
)
|
249
|
+
|
250
|
+
parser.add_argument(
|
251
|
+
"-hr",
|
252
|
+
"--html-report",
|
253
|
+
required=False,
|
254
|
+
action="store_true",
|
255
|
+
help="Creates an HTML report of the errors, called <file-name>.errors.html,"
|
256
|
+
" alongside the original file or in a .amati/ directory if the --local switch"
|
257
|
+
" is used",
|
258
|
+
)
|
259
|
+
|
199
260
|
args = parser.parse_args()
|
200
261
|
|
201
262
|
if args.spec:
|
@@ -204,4 +265,9 @@ if __name__ == "__main__":
|
|
204
265
|
specifications = discover(args.discover)
|
205
266
|
|
206
267
|
for specification in specifications:
|
207
|
-
run(
|
268
|
+
if successful_check := run(
|
269
|
+
specification, args.consistency_check, args.local, args.html_report
|
270
|
+
):
|
271
|
+
print("Consistency check successful for {specification}")
|
272
|
+
else:
|
273
|
+
print("Consistency check failed for {specification}")
|
@@ -4,8 +4,6 @@ Exceptions, declared here to not put in __init__
|
|
4
4
|
|
5
5
|
from typing import Optional
|
6
6
|
|
7
|
-
from amati.references import References
|
8
|
-
|
9
7
|
|
10
8
|
class AmatiValueError(ValueError):
|
11
9
|
"""
|
@@ -21,6 +19,6 @@ class AmatiValueError(ValueError):
|
|
21
19
|
ValueError
|
22
20
|
"""
|
23
21
|
|
24
|
-
def __init__(self, message: str,
|
22
|
+
def __init__(self, message: str, reference_uri: Optional[str] = None):
|
25
23
|
self.message = message
|
26
|
-
self.
|
24
|
+
self.reference_uri = reference_uri
|
@@ -5,14 +5,10 @@ Validates an email according to the RFC5322 ABNF grammar - §3:
|
|
5
5
|
from abnf import ParseError
|
6
6
|
from abnf.grammars import rfc5322
|
7
7
|
|
8
|
-
from amati import AmatiValueError
|
8
|
+
from amati import AmatiValueError
|
9
9
|
from amati.fields import Str as _Str
|
10
10
|
|
11
|
-
|
12
|
-
title="Internet Message Format",
|
13
|
-
url="https://www.rfc-editor.org/rfc/rfc5322#section-3",
|
14
|
-
section="Syntax",
|
15
|
-
)
|
11
|
+
reference_uri = "https://www.rfc-editor.org/rfc/rfc5322#section-3"
|
16
12
|
|
17
13
|
|
18
14
|
class Email(_Str):
|
@@ -23,5 +19,5 @@ class Email(_Str):
|
|
23
19
|
rfc5322.Rule("address").parse_all(value)
|
24
20
|
except ParseError as e:
|
25
21
|
raise AmatiValueError(
|
26
|
-
|
22
|
+
f"{value} is not a valid email address", reference_uri
|
27
23
|
) from e
|