amati 0.2.29__py3-none-any.whl → 0.3.1__py3-none-any.whl

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/_logging.py CHANGED
@@ -10,7 +10,7 @@ from typing import Any, ClassVar, NotRequired, TypedDict
10
10
  type LogType = Exception | Warning
11
11
 
12
12
 
13
- @dataclass
13
+ @dataclass(frozen=True)
14
14
  class Log(TypedDict):
15
15
  type: str
16
16
  loc: NotRequired[tuple[int | str, ...]]
@@ -20,11 +20,10 @@ class Log(TypedDict):
20
20
 
21
21
 
22
22
  class Logger:
23
- """
24
- A mixin class that provides logging functionality.
23
+ """A simple class-level logger for collecting Log objects.
25
24
 
26
- This class maintains a list of Log messages that are added.
27
- It is NOT thread-safe. State is maintained at a global level.
25
+ This class provides methods for appending logs and managing
26
+ a logging context that automatically clears the logs.
28
27
  """
29
28
 
30
29
  logs: ClassVar[list[Log]] = []
amati/amati.py CHANGED
@@ -163,86 +163,39 @@ def run(
163
163
  return True
164
164
 
165
165
 
166
- def discover(spec: str, discover_dir: str = ".") -> list[Path]:
167
- """
168
- Finds OpenAPI Specification files to validate
169
-
170
- Args:
171
- spec: The path to a specific OpenAPI specification file.
172
- discover_dir: The directory to search through.
173
- Returns:
174
- A list of specifications to validate.
175
- """
176
-
177
- specs: list[Path] = []
178
-
179
- # If a spec is provided, check if it exists and erorr if not
180
- if spec:
181
- spec_path = Path(spec)
182
-
183
- if not spec_path.exists():
184
- raise FileNotFoundError(f"File {spec} does not exist.")
185
-
186
- if not spec_path.is_file():
187
- raise IsADirectoryError(f"{spec} is a directory, not a file.")
188
-
189
- specs.append(spec_path)
190
-
191
- # End early if we're not also trying to find files
192
- if not discover_dir:
193
- return specs
194
-
195
- if Path("openapi.json").exists():
196
- specs.append(Path("openapi.json"))
197
-
198
- if Path("openapi.yaml").exists():
199
- specs.append(Path("openapi.yaml"))
200
-
201
- if specs:
202
- return specs
203
-
204
- if discover_dir == ".":
205
- raise FileNotFoundError(
206
- "openapi.json or openapi.yaml can't be found, use --discover or --spec."
207
- )
208
-
209
- specs = specs + list(Path(discover_dir).glob("**/openapi.json"))
210
- specs = specs + list(Path(discover_dir).glob("**/openapi.yaml"))
211
-
212
- if not specs:
213
- raise FileNotFoundError(
214
- "openapi.json or openapi.yaml can't be found, use --spec."
215
- )
216
-
217
- return specs
218
-
219
-
220
166
  if __name__ == "__main__":
167
+ logger.remove() # Remove the default logger
168
+ # Add a new logger that outputs to stderr with a specific format
169
+ logger.add(sys.stderr, format="{time} | {level} | {message}")
170
+
221
171
  import argparse
222
172
 
223
173
  parser: argparse.ArgumentParser = argparse.ArgumentParser(
224
174
  prog="amati",
225
175
  description="""
226
- Tests whether a OpenAPI specification is valid. Will look an openapi.json
227
- or openapi.yaml file in the directory that amati is called from. If
228
- --discover is set will search the directory tree. If the specification
229
- does not follow the naming recommendation the --spec switch should be
230
- used.
231
-
232
- Creates a file <filename>.errors.json alongside the original specification
233
- containing a JSON representation of all the errors.
176
+ Tests whether a OpenAPI specification is valid. Creates a file
177
+ <filename>.errors.json alongside the original specification containing
178
+ a JSON representation of all the errors.
179
+
180
+ Optionally creates an HTML report of the errors, and performs an internal
181
+ consistency check to verify that the output of the validation is identical
182
+ to the input.
234
183
  """,
235
184
  suggest_on_error=True,
236
185
  )
237
186
 
238
- parser.add_argument(
187
+ subparsers: argparse.Action = parser.add_subparsers(required=True, dest="command")
188
+
189
+ validation: argparse.ArgumentParser = subparsers.add_parser("validate")
190
+
191
+ validation.add_argument(
239
192
  "-s",
240
193
  "--spec",
241
- required=False,
242
- help="The specification to be parsed",
194
+ required=True,
195
+ help="The specification to be validated",
243
196
  )
244
197
 
245
- parser.add_argument(
198
+ validation.add_argument(
246
199
  "--consistency-check",
247
200
  required=False,
248
201
  action="store_true",
@@ -250,48 +203,44 @@ if __name__ == "__main__":
250
203
  " parsed specification",
251
204
  )
252
205
 
253
- parser.add_argument(
254
- "-d",
255
- "--discover",
256
- required=False,
257
- default=".",
258
- help="Searches the specified directory tree for openapi.yaml or openapi.json.",
259
- )
260
-
261
- parser.add_argument(
206
+ validation.add_argument(
262
207
  "--local",
263
208
  required=False,
264
209
  action="store_true",
265
- help="Store errors local to the caller in a file called <file-name>.errors.json"
266
- "; a .amati/ directory will be created.",
210
+ help="Store errors local to the caller in .amati/<file-name>.errors.json",
267
211
  )
268
212
 
269
- parser.add_argument(
213
+ validation.add_argument(
270
214
  "--html-report",
271
215
  required=False,
272
216
  action="store_true",
273
217
  help="Creates an HTML report of the errors, called <file-name>.errors.html,"
274
- " alongside the original file or in a .amati/ directory if the --local switch"
275
- " is used",
218
+ " alongside <filename>.errors.json",
276
219
  )
277
220
 
278
- parser.add_argument(
279
- "--refresh-data",
221
+ refreshment: argparse.ArgumentParser = subparsers.add_parser("refresh")
222
+
223
+ refreshment.add_argument(
224
+ "--type",
280
225
  required=False,
281
- action="store_true",
282
- help="Refreshes the local data files used by amati, such as HTTP status codes "
283
- "or , media types from IANA",
226
+ default="all",
227
+ choices=[
228
+ "all",
229
+ "http_status_code",
230
+ "iso9110",
231
+ "media_types",
232
+ "schemes",
233
+ "spdx_licences",
234
+ "tlds",
235
+ ],
236
+ help="The type of data to refresh. Defaults to all.",
284
237
  )
285
238
 
286
239
  args: argparse.Namespace = parser.parse_args()
287
240
 
288
- logger.remove() # Remove the default logger
289
- # Add a new logger that outputs to stderr with a specific format
290
- logger.add(sys.stderr, format="{time} | {level} | {message}")
291
-
292
241
  logger.info("Starting amati")
293
242
 
294
- if args.refresh_data:
243
+ if args.command == "refresh":
295
244
  logger.info("Refreshing data.")
296
245
  try:
297
246
  refresh("all")
@@ -301,32 +250,24 @@ if __name__ == "__main__":
301
250
  logger.error(f"Error refreshing data: {str(e)}")
302
251
  sys.exit(1)
303
252
 
253
+ specification: Path = Path(args.spec)
254
+ logger.info(f"Processing specification {specification}")
255
+
256
+ # Top-level try/except to ensure one failed spec doesn't stop the rest
257
+ # from being processed.
258
+ e: Exception
304
259
  try:
305
- specifications: list[Path] = discover(args.spec, args.discover)
260
+ successful_check: bool = run(
261
+ specification, args.consistency_check, args.local, args.html_report
262
+ )
263
+ logger.info(f"Specification {specification} processed successfully.")
306
264
  except Exception as e:
307
- logger.error(str(e))
265
+ logger.error(f"Error processing {specification}, {str(e)}")
308
266
  sys.exit(1)
309
267
 
310
- specification: Path
311
- for specification in specifications:
312
- successful_check: bool = False
313
- logger.info(f"Processing specification {specification}")
314
-
315
- # Top-level try/except to ensure one failed spec doesn't stop the rest
316
- # from being processed.
317
- e: Exception
318
- try:
319
- successful_check = run(
320
- specification, args.consistency_check, args.local, args.html_report
321
- )
322
- logger.info(f"Specification {specification} processed successfully.")
323
- except Exception as e:
324
- logger.error(f"Error processing {specification}, {str(e)}")
325
- sys.exit(1)
326
-
327
- if args.consistency_check and successful_check:
328
- logger.info(f"Consistency check successful for {specification}")
329
- elif args.consistency_check:
330
- logger.info(f"Consistency check failed for {specification}")
268
+ if args.consistency_check and successful_check:
269
+ logger.info(f"Consistency check successful for {specification}")
270
+ elif args.consistency_check:
271
+ logger.info(f"Consistency check failed for {specification}")
331
272
 
332
273
  logger.info("Stopping amati.")
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amati
3
- Version: 0.2.29
3
+ Version: 0.3.1
4
4
  Summary: Validates that a .yaml or .json file conforms to the OpenAPI Specifications 3.x.
5
- Project-URL: Homepage, https://github.com/gwyil/amati
6
- Project-URL: Issues, https://github.com/gwyil/amati/issues
5
+ Project-URL: Homepage, https://github.com/gwyli/amati
6
+ Project-URL: Issues, https://github.com/gwyli/amati/issues
7
7
  Author-email: Ben <2551337+ben-alexander@users.noreply.github.com>
8
8
  License-File: LICENSE
9
9
  Classifier: Development Status :: 3 - Alpha
@@ -42,42 +42,33 @@ amati is designed to validate that a file conforms to the [OpenAPI Specification
42
42
  ## Usage
43
43
 
44
44
  ```sh
45
- python amati/amati.py --help
46
- usage: amati [-h] [-s SPEC] [--consistency-check] [-d DISCOVER] [--local] [--html-report]
47
-
48
- Tests whether a OpenAPI specification is valid. Will look an openapi.json or openapi.yaml file in the directory that
49
- amati is called from. If --discover is set will search the directory tree. If the specification does not follow the
50
- naming recommendation the --spec switch should be used. Creates a file <filename>.errors.json alongside the original
51
- specification containing a JSON representation of all the errors.
45
+ python amati/amati.py validate --help
46
+ usage: amati validate [-h] -s SPEC [--consistency-check] [--local] [--html-report]
52
47
 
53
48
  options:
54
49
  -h, --help show this help message and exit
55
- -s, --spec SPEC The specification to be parsed
50
+ -s, --spec SPEC The specification to be validated
56
51
  --consistency-check Runs a consistency check between the input specification and the parsed specification
57
- -d, --discover DISCOVER
58
- Searches the specified directory tree for openapi.yaml or openapi.json.
59
- --local Store errors local to the caller in a file called <file-name>.errors.json; a .amati/ directory
60
- will be created.
61
- --html-report Creates an HTML report of the errors, called <file-name>.errors.html, alongside the original
62
- file or in a .amati/ directory if the --local switch is used
52
+ --local Store errors local to the caller in .amati/<file-name>.errors.json
53
+ --html-report Creates an HTML report of the errors, called <file-name>.errors.html, alongside <filename>.errors.json
63
54
  ```
64
55
 
65
56
  ### Docker
66
57
 
67
58
  A Dockerfile is available on [DockerHub](https://hub.docker.com/r/benale/amati/tags) or `docker pull benale/amati:alpha`.
68
59
 
69
- Whilst an alpha build only the image tagged `alpha` will be maintained. If there are breaking API changes these will be detailed in releases going forward. Releases can be separately watched using the custom option when watching this repository.
60
+ Whilst an alpha build, only the image tagged `alpha` will be maintained. If there are breaking API changes these will be detailed in releases. Releases can be separately watched using the custom option when watching this repository.
70
61
 
71
62
  To run against a specific specification the location of the specification needs to be mounted in the container.
72
63
 
73
64
  ```sh
74
- docker run -v "<path-to-specification>:/<mount-name> benale/amati:alpha <options>
65
+ docker run -v "<path-to-specification>:/<mount-name> benale/amati:alpha validate --spec <path-to-spec> <options>
75
66
  ```
76
67
 
77
68
  e.g. where you have a specification located in `/Users/myuser/myrepo/myspec.yaml` and create a mount `/data`:
78
69
 
79
70
  ```sh
80
- docker run -v /Users/myuser/myrepo:/data benale/amati:alpha --spec /data/myspec.yaml --html-report
71
+ docker run -v /Users/myuser/myrepo:/data benale/amati:alpha validate --spec /data/myspec.yaml --html-report
81
72
  ```
82
73
 
83
74
  ### PyPI
@@ -86,14 +77,13 @@ amati is [available on PyPI](https://pypi.org/project/amati/), to run everything
86
77
 
87
78
  ```py
88
79
  >>> from amati import amati
89
- >>> check = amati.run('tests/data/openapi.yaml', consistency_check=True, local=True, html_report=True)
90
- >>> check
80
+ >>> amati.run('tests/data/openapi.yaml', consistency_check=True, local=True, html_report=True)
91
81
  True
92
82
  ```
93
83
 
94
84
  ## Architecture
95
85
 
96
- 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).
86
+ amati uses [Pydantic](https://docs.pydantic.dev/latest/), especially the validation, and [typing](https://docs.python.org/3/library/typing.html) 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).
97
87
 
98
88
  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.
99
89
 
@@ -130,7 +120,7 @@ It's expected that there are no errors and 100% of the code is reached and execu
130
120
  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/amati-tests-specs/<repo-name>`.
131
121
 
132
122
  ```sh
133
- python scripts/tests/setup_test_specs.py
123
+ python scripts/setup_test_specs.py
134
124
  ```
135
125
 
136
126
  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.
@@ -152,18 +142,22 @@ docker build -t amati -f Dockerfile .
152
142
  to run against a specific specification the location of the specification needs to be mounted in the container.
153
143
 
154
144
  ```sh
155
- docker run -v "<path-to-specification>:/<mount-name> amati <options>
145
+ docker run -v "<path-to-specification>:/<mount-name> amati validate -s <path-to-spec> <options>
156
146
  ```
157
147
 
158
148
  This can be tested against a provided specification, from the root directory
159
149
 
160
150
  ```sh
161
- docker run --detach -v "$(pwd):/data" amati <options>
151
+ docker run --detach -v "$(pwd):/data" amati validate -s <path-to-spec> <options>
162
152
  ```
163
153
 
164
154
 
165
155
  ### Data
166
156
 
167
- 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`.
157
+ There are some scripts to create the data needed by the project, for example, all the registered TLDs. To refresh the data, run:
158
+
159
+ ```py
160
+ python amati/amati.py refresh
161
+ ```
168
162
 
169
163
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/gwyli/amati/badge)](https://scorecard.dev/viewer/?uri=github.com/gwyli/amati)
@@ -1,8 +1,8 @@
1
1
  amati/__init__.py,sha256=IXbWlVtFGTIdvxaafs8Qy8zeD3KjTDgK0omllFq8Jio,461
2
2
  amati/_error_handler.py,sha256=_zs0hWqNf6NlXPk9-2MWyk6t5QGttDxNqatZ5YnCm6U,1245
3
- amati/_logging.py,sha256=y7D0YwYleXx-8ainr-ydDHe4usykI9Fw_c0b90p7tqQ,1352
3
+ amati/_logging.py,sha256=1q5mPlnZDSgAHl56lB20ZN6kV6OS-ew-8fvPeXF0uKI,1357
4
4
  amati/_resolve_forward_references.py,sha256=-9MORpUhgusGJdvnH502p8-dc9Q6zqaB5XkPZvp4o7E,6509
5
- amati/amati.py,sha256=Lj-cEBNqOHuxGIEegYSlUPH6UpkFUixJrfpTQAal86s,9911
5
+ amati/amati.py,sha256=_oFo_Z9cuS7NdN7WZTynXlTDMzndXuT4ERznGWaNpdw,8155
6
6
  amati/exceptions.py,sha256=MUzW7FsE0_4BJC99hStokMItKk7OvNBiqO6Zr6d_8cY,577
7
7
  amati/file_handler.py,sha256=59kAsyVHE8mddRrNgyEowgZLEsgLcsZsFq6GtH7ZYBo,8065
8
8
  amati/model_validators.py,sha256=py9QWEa68bE_tv9JrfnB2nJz16f4_Z2gLQAhH4O-CCs,14887
@@ -38,8 +38,8 @@ amati/validators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
38
38
  amati/validators/generic.py,sha256=NM6ZkuGdM8SDvSUBbMcVz2KIYBWFH2lYTFMVpS1I5QM,5270
39
39
  amati/validators/oas304.py,sha256=suDonNEMIzOmX_JfJFT1kYTT0dFFcSHsk8qOpf_x80A,31967
40
40
  amati/validators/oas311.py,sha256=ey_j-6YR0rkYd6arVkeK1fDLsGBBoyWjjYBUy7Wj2xM,17325
41
- amati-0.2.29.dist-info/METADATA,sha256=NdyMzfa9yYWIWEnTk6-2jzQ1pK6D_GPhKdhXKLasz0s,7444
42
- amati-0.2.29.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
- amati-0.2.29.dist-info/entry_points.txt,sha256=sacBb6g0f0ZJtNjNYx93_Xe4y5xzawvklCFVXup9ru0,37
44
- amati-0.2.29.dist-info/licenses/LICENSE,sha256=WAA01ZXeNs1bwpNWKR6aVucjtYjYm_iQIUYkCAENjqM,1070
45
- amati-0.2.29.dist-info/RECORD,,
41
+ amati-0.3.1.dist-info/METADATA,sha256=B5EM86jqsymAgnVDUMT-bPPrt6IKGeQbk75iQA1wxbI,6918
42
+ amati-0.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
+ amati-0.3.1.dist-info/entry_points.txt,sha256=sacBb6g0f0ZJtNjNYx93_Xe4y5xzawvklCFVXup9ru0,37
44
+ amati-0.3.1.dist-info/licenses/LICENSE,sha256=WAA01ZXeNs1bwpNWKR6aVucjtYjYm_iQIUYkCAENjqM,1070
45
+ amati-0.3.1.dist-info/RECORD,,
File without changes