amati 0.1.0__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.
@@ -0,0 +1,615 @@
1
+ """
2
+ Validates the OpenAPI Specification version 3.1.1
3
+
4
+ Note that per https://spec.openapis.org/oas/v3.1.1.html#relative-references-in-api-description-uris # pylint: disable=line-too-long
5
+
6
+ > URIs used as references within an OpenAPI Description, or to external documentation
7
+ > or other supplementary information such as a license, are resolved as identifiers,
8
+ > and described by this specification as URIs.
9
+
10
+ > Note that some URI fields are named url for historical reasons, but the descriptive
11
+ > text for those fields uses the correct “URI” terminology.
12
+
13
+ """
14
+
15
+ import re
16
+ from typing import Any, ClassVar, Optional
17
+ from typing_extensions import Self
18
+
19
+ from jsonschema.exceptions import ValidationError as JSONVSchemeValidationError
20
+ from jsonschema.protocols import Validator as JSONSchemaValidator
21
+ from jsonschema.validators import validator_for # type: ignore
22
+ from pydantic import (
23
+ ConfigDict,
24
+ Field,
25
+ RootModel,
26
+ ValidationError,
27
+ model_validator,
28
+ )
29
+
30
+ from amati import AmatiValueError, Reference
31
+ from amati import model_validators as mv
32
+ from amati.fields import (
33
+ SPDXURL,
34
+ URI,
35
+ HTTPStatusCode,
36
+ SPDXIdentifier,
37
+ )
38
+ from amati.fields.commonmark import CommonMark
39
+ from amati.fields.json import JSON
40
+ from amati.fields.oas import OpenAPI
41
+ from amati.fields.spdx_licences import VALID_LICENCES
42
+ from amati.logging import Log, LogMixin
43
+ from amati.validators.generic import GenericObject, allow_extra_fields
44
+ from amati.validators.oas304 import (
45
+ CallbackObject,
46
+ ContactObject,
47
+ EncodingObject,
48
+ ExampleObject,
49
+ ExternalDocumentationObject,
50
+ HeaderObject,
51
+ LinkObject,
52
+ PathItemObject,
53
+ PathsObject,
54
+ RequestBodyObject,
55
+ ResponseObject,
56
+ )
57
+ from amati.validators.oas304 import SecuritySchemeObject as OAS30SecuritySchemeObject
58
+ from amati.validators.oas304 import (
59
+ ServerObject,
60
+ TagObject,
61
+ XMLObject,
62
+ )
63
+
64
+ TITLE = "OpenAPI Specification v3.1.1"
65
+
66
+ # Convenience naming to ensure that it's clear what's happening.
67
+ # https://spec.openapis.org/oas/v3.1.1.html#specification-extensions
68
+ specification_extensions = allow_extra_fields
69
+
70
+
71
+ @specification_extensions("x-")
72
+ class LicenceObject(GenericObject):
73
+ """
74
+ A model representing the OpenAPI Specification licence object §4.8.4
75
+
76
+ OAS uses the SPDX licence list.
77
+
78
+ # SPECFIX: The URI is mutually exclusive of the identifier. I don't see
79
+ the purpose of this; if the identifier is a SPDX Identifier where's the
80
+ harm in also including the URI
81
+ """
82
+
83
+ name: str = Field(min_length=1)
84
+ # What difference does Optional make here?
85
+ identifier: Optional[SPDXIdentifier] = None
86
+ url: Optional[URI] = None
87
+ _reference: ClassVar[Reference] = Reference(
88
+ title=TITLE,
89
+ url="https://spec.openapis.org/oas/v3.1.1.html#license-object",
90
+ section="License Object",
91
+ )
92
+
93
+ _not_url_and_identifier = mv.only_one_of(["url", "identifier"])
94
+
95
+ @model_validator(mode="after")
96
+ def check_uri_associated_with_identifier(self: Self) -> Self:
97
+ """
98
+ Validate that the URL matches the provided licence identifier.
99
+
100
+ This validator checks if the URL is listed among the known URLs for the
101
+ specified licence identifier.
102
+
103
+ Returns:
104
+ The validated licence object
105
+ """
106
+ # URI only - should warn if not SPDX
107
+ if self.url:
108
+ try:
109
+ SPDXURL(self.url)
110
+ except AmatiValueError:
111
+ LogMixin.log(
112
+ Log(
113
+ message=f"{self.url} is not a valid SPDX URL",
114
+ type=Warning,
115
+ reference=self._reference,
116
+ )
117
+ )
118
+
119
+ # Both Identifier and URI, technically invalid, but should check if
120
+ # consistent
121
+ if (
122
+ self.url
123
+ and self.identifier
124
+ and str(self.url) not in VALID_LICENCES[self.identifier]
125
+ ):
126
+ LogMixin.log(
127
+ Log(
128
+ message=f"{self.url} is not associated with the identifier {self.identifier}", # pylint: disable=line-too-long
129
+ type=Warning,
130
+ reference=self._reference,
131
+ )
132
+ )
133
+
134
+ return self
135
+
136
+
137
+ class ReferenceObject(GenericObject):
138
+ """
139
+ Validates the OpenAPI Specification reference object - §4.8.23
140
+
141
+ Note, "URIs" can be prefixed with a hash; this is because if the
142
+ representation of the referenced document is JSON or YAML, then
143
+ the fragment identifier SHOULD be interpreted as a JSON-Pointer
144
+ as per RFC6901.
145
+ """
146
+
147
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
148
+
149
+ ref: URI = Field(alias="$ref")
150
+ summary: Optional[str]
151
+ description: Optional[CommonMark]
152
+ _reference: ClassVar[Reference] = Reference(
153
+ title=TITLE,
154
+ url="https://spec.openapis.org/oas/v3.1.1.html#reference-object",
155
+ section="Reference Object",
156
+ )
157
+
158
+
159
+ @specification_extensions("x-")
160
+ class InfoObject(GenericObject):
161
+ """
162
+ Validates the OpenAPI Specification info object - §4.8.2:
163
+ """
164
+
165
+ title: str
166
+ summary: Optional[str] = None
167
+ description: Optional[str | CommonMark] = None
168
+ termsOfService: Optional[str] = None # pylint: disable=invalid-name
169
+ contact: Optional[ContactObject] = None
170
+ license: Optional[LicenceObject] = None
171
+ version: str
172
+ _reference: ClassVar[Reference] = Reference(
173
+ title=TITLE,
174
+ url="https://spec.openapis.org/oas/3.1.1.html#info-object",
175
+ section="Info Object",
176
+ )
177
+
178
+
179
+ @specification_extensions("x-")
180
+ class DiscriminatorObject(GenericObject):
181
+ """
182
+ Validates the OpenAPI Specification object - §4.8.25
183
+ """
184
+
185
+ # FIXME: Need post processing to determine whether the property actually exists
186
+ # FIXME: The component and schema objects need to check that this is being used
187
+ # properly.
188
+ propertyName: str
189
+ mapping: Optional[dict[str, str | URI]] = None
190
+ _reference: ClassVar[Reference] = Reference(
191
+ title=TITLE,
192
+ url="https://spec.openapis.org/oas/v3.1.1.html#discriminator-object",
193
+ section="Discriminator Object",
194
+ )
195
+
196
+
197
+ @specification_extensions("x-")
198
+ class ServerVariableObject(GenericObject):
199
+ """
200
+ Validates the OpenAPI Specification server variable object - §4.8.6
201
+ """
202
+
203
+ enum: Optional[list[str]] = Field(None, min_length=1)
204
+ default: str = Field(min_length=1)
205
+ description: Optional[str | CommonMark] = None
206
+ _reference: ClassVar[Reference] = Reference(
207
+ title=TITLE,
208
+ url="https://spec.openapis.org/oas/v3.1.1.html#server-variable-object",
209
+ section="Server Variable Object",
210
+ )
211
+
212
+ @model_validator(mode="after")
213
+ def check_enum_default(self: Self) -> Self:
214
+ """
215
+ Validate that the default value is in the enum list.
216
+
217
+ Returns:
218
+ The validated server variable object
219
+ """
220
+ if self.enum is None:
221
+ return self
222
+
223
+ if self.default not in self.enum:
224
+ LogMixin.log(
225
+ Log(
226
+ message=f"The default value {self.default} is not in the enum list {self.enum}", # pylint: disable=line-too-long
227
+ type=ValueError,
228
+ reference=self._reference,
229
+ )
230
+ )
231
+
232
+ return self
233
+
234
+
235
+ @specification_extensions("x-")
236
+ class OperationObject(GenericObject):
237
+ """Validates the OpenAPI Specification operation object - §4.8.10"""
238
+
239
+ tags: Optional[list[str]] = None
240
+ summary: Optional[str] = None
241
+ description: Optional[str | CommonMark] = None
242
+ externalDocs: Optional[ExternalDocumentationObject] = None
243
+ operationId: Optional[str] = None
244
+ parameters: Optional[list["ParameterObject | ReferenceObject"]] = None
245
+ requestBody: Optional["RequestBodyObject | ReferenceObject"] = None
246
+ responses: Optional["ResponsesObject"] = None
247
+ callbacks: Optional[dict[str, "CallbackObject | ReferenceObject"]] = None
248
+ deprecated: Optional[bool] = False
249
+ security: Optional[list["SecurityRequirementObject"]] = None
250
+ servers: Optional[list[ServerObject]] = None
251
+
252
+ _reference: ClassVar[Reference] = Reference(
253
+ title=TITLE,
254
+ url="https://spec.openapis.org/oas/v3.1.1.html#operation-object",
255
+ section="Operation Object",
256
+ )
257
+
258
+
259
+ PARAMETER_STYLES: set[str] = {
260
+ "matrix",
261
+ "label",
262
+ "simple",
263
+ "form",
264
+ "spaceDelimited",
265
+ "pipeDelimited",
266
+ "deepObject",
267
+ }
268
+
269
+
270
+ class ParameterObject(GenericObject):
271
+ """Validates the OpenAPI Specification parameter object - §4.8.11"""
272
+
273
+ name: str
274
+ in_: str = Field(alias="in")
275
+ description: Optional[str | CommonMark] = None
276
+ required: Optional[bool] = None
277
+ deprecated: Optional[bool] = None
278
+ allowEmptyValue: Optional[bool] = None
279
+ style: Optional[str] = None
280
+ explode: Optional[bool] = None
281
+ allowReserved: Optional[bool] = None
282
+ schema_: Optional["SchemaObject"] = Field(alias="schema")
283
+ example: Optional[Any] = None
284
+ examples: Optional[dict[str, "ExampleObject | ReferenceObject"]] = None
285
+ content: Optional[dict[str, "MediaTypeObject"]] = None
286
+
287
+ _in_valid = mv.if_then(
288
+ conditions={"in_": mv.UNKNOWN},
289
+ consequences={"in_": ["query", "header", "path", "cookie"]},
290
+ )
291
+ _path_location_is_required = mv.if_then(
292
+ conditions={"in_": "path"}, consequences={"required": True}
293
+ )
294
+ _empty_value_only_with_query = mv.if_then(
295
+ conditions={"allowEmptyValue": mv.UNKNOWN},
296
+ consequences={"in_": PARAMETER_STYLES ^ {"query"}},
297
+ )
298
+ _style_is_valid = mv.if_then(
299
+ conditions={"style": mv.UNKNOWN}, consequences={"style": list(PARAMETER_STYLES)}
300
+ )
301
+ _reserved_only_with_query = mv.if_then(
302
+ conditions={"allowReserved": mv.UNKNOWN},
303
+ consequences={"in_": PARAMETER_STYLES ^ {"query"}},
304
+ )
305
+ _disallowed_if_schema = mv.if_then(
306
+ conditions={"schema_": mv.UNKNOWN}, consequences={"content": None}
307
+ )
308
+ _disallowed_if_content = mv.if_then(
309
+ conditions={"content": mv.UNKNOWN},
310
+ consequences={
311
+ "content": None,
312
+ "style": None,
313
+ "explode": None,
314
+ "allowReserved": None,
315
+ "schema_": None,
316
+ },
317
+ )
318
+
319
+
320
+ @specification_extensions("x-")
321
+ class MediaTypeObject(GenericObject):
322
+ """
323
+ Validates the OpenAPI Specification media type object - §4.8.14
324
+ """
325
+
326
+ schema_: Optional["SchemaObject"] = Field(alias="schema", default=None)
327
+ # FIXME: Define example
328
+ example: Optional[Any] = None
329
+ examples: Optional[dict[str, ExampleObject | ReferenceObject]] = None
330
+ encoding: Optional["EncodingObject"] = None
331
+ _reference: ClassVar[Reference] = Reference(
332
+ title=TITLE,
333
+ url="https://spec.openapis.org/oas/v3.1.1.html#media-type-object",
334
+ section="Tag Object",
335
+ )
336
+
337
+
338
+ type _ResponsesObjectReturnType = dict[str, "ReferenceObject | ResponseObject"]
339
+
340
+
341
+ @specification_extensions("x-")
342
+ class ResponsesObject(GenericObject):
343
+ """
344
+ Validates the OpenAPI Specification responses object - §4.8.16
345
+ """
346
+
347
+ model_config = ConfigDict(
348
+ extra="allow",
349
+ )
350
+
351
+ default: Optional["ResponseObject | ReferenceObject"] = None
352
+ _reference: ClassVar[Reference] = Reference(
353
+ title=TITLE,
354
+ url="https://spec.openapis.org/oas/v3.1.1.html#responses-object",
355
+ section="Responses Object",
356
+ )
357
+
358
+ @classmethod
359
+ def _choose_model(
360
+ cls, value: Any, field_name: str
361
+ ) -> "ReferenceObject | ResponseObject":
362
+ """
363
+ Choose the model to use for validation based on the type of value.
364
+
365
+ Args:
366
+ value: The value to validate.
367
+
368
+ Returns:
369
+ The model class to use for validation.
370
+ """
371
+
372
+ message = f"{field_name} must be a ResponseObject or ReferenceObject, got {type(value)}" # pylint: disable=line-too-long
373
+
374
+ try:
375
+ return ResponseObject.model_validate(value)
376
+ except ValidationError:
377
+ try:
378
+ return ReferenceObject.model_validate(value)
379
+ except ValidationError as e:
380
+ raise ValueError(message, ResponsesObject._reference) from e
381
+
382
+ @model_validator(mode="before")
383
+ @classmethod
384
+ def validate_all_fields(cls, data: dict[str, Any]) -> _ResponsesObjectReturnType:
385
+ """
386
+ Validates the responses object.
387
+ """
388
+
389
+ validated_data: _ResponsesObjectReturnType = {}
390
+
391
+ for field_name, value in data.items():
392
+
393
+ # If the value is a specification extension, allow it
394
+ if field_name.startswith("x-"):
395
+ validated_data[field_name] = value
396
+ continue
397
+
398
+ # If the value is the fixed field, "default", allow it
399
+ if field_name == "default":
400
+ if isinstance(value, dict):
401
+ validated_data[field_name] = ResponsesObject._choose_model(
402
+ value, field_name
403
+ )
404
+ continue
405
+
406
+ # Otherwise, if the field appears like a valid HTTP status code or a range
407
+ if re.match(r"^[1-5]([0-9]{2}|XX)+$", str(field_name)):
408
+
409
+ # Double check and raise a value error if not
410
+ HTTPStatusCode(field_name)
411
+
412
+ # and validate as a ResponseObject or ReferenceObject
413
+ validated_data[field_name] = ResponsesObject._choose_model(
414
+ value, field_name
415
+ )
416
+
417
+ continue
418
+
419
+ # If the field is not a valid HTTP status code or "default"
420
+ raise ValueError(f"Invalid type for numeric field '{field_name}'")
421
+
422
+ return validated_data
423
+
424
+
425
+ class SchemaObject(GenericObject):
426
+ """
427
+ Schema Object as per OAS 3.1.1 specification (section 4.8.24)
428
+
429
+ This model defines only the OpenAPI-specific fields explicitly.
430
+ Standard JSON Schema fields are allowed via the 'extra' config
431
+ and validated through jsonschema.
432
+ """
433
+
434
+ model_config = ConfigDict(
435
+ populate_by_name=True,
436
+ extra="allow", # Allow all standard JSON Schema fields
437
+ )
438
+
439
+ # OpenAPI-specific fields not in standard JSON Schema
440
+ nullable: Optional[bool] = None # OAS 3.0 style nullable flag
441
+ discriminator: Optional[DiscriminatorObject] = None # Polymorphism support
442
+ readOnly: Optional[bool] = None # Declares property as read-only for requests
443
+ writeOnly: Optional[bool] = None # Declares property as write-only for responses
444
+ xml: Optional[XMLObject] = None # XML metadata
445
+ externalDocs: Optional[ExternalDocumentationObject] = None # External documentation
446
+ example: Optional[Any] = None # Example of schema
447
+ examples: Optional[list[Any]] = None # Examples of schema (OAS 3.1)
448
+ deprecated: Optional[bool] = None # Specifies schema is deprecated
449
+
450
+ # JSON Schema fields that need special handling in OAS context
451
+ ref: Optional[str] = Field(
452
+ default=None, alias="$ref"
453
+ ) # Reference to another schema
454
+
455
+ _reference: ClassVar[Reference] = Reference(
456
+ title=TITLE,
457
+ url="https://spec.openapis.org/oas/v3.1.1.html#schema-object",
458
+ section="Link Object",
459
+ )
460
+
461
+ @model_validator(mode="after")
462
+ def validate_schema(self):
463
+ """
464
+ Use jsonschema to validate the model as a valid JSON Schema
465
+ """
466
+ schema_dict = self.model_dump(exclude_none=True, by_alias=True)
467
+
468
+ # Handle OAS 3.1 specific validations
469
+
470
+ # 1. Convert nullable to type array with null if needed
471
+ if schema_dict.get("nullable") is True and "type" in schema_dict:
472
+ type_val = schema_dict["type"]
473
+ if isinstance(type_val, str) and type_val != "null":
474
+ schema_dict["type"] = [type_val, "null"]
475
+ elif isinstance(type_val, list) and "null" not in type_val:
476
+ schema_dict["type"] = type_val + ["null"]
477
+
478
+ # 2. Validate the schema structure using jsonschema's meta-schema
479
+ # Get the right validator based on the declared $schema or default
480
+ # to Draft 2020-12
481
+ schema_version = schema_dict.get(
482
+ "$schema", "https://json-schema.org/draft/2020-12/schema"
483
+ )
484
+ try:
485
+ validator_cls: JSONSchemaValidator = validator_for( # type: ignore
486
+ {"$schema": schema_version}
487
+ )
488
+ meta_schema: JSON = validator_cls.META_SCHEMA # type: ignore
489
+
490
+ # This will validate the structure conforms to JSON Schema
491
+ validator_cls(meta_schema).validate(schema_dict) # type: ignore
492
+ except JSONVSchemeValidationError as e:
493
+ LogMixin.log(
494
+ Log(
495
+ message=f"Invalid JSON Schema: {e.message}",
496
+ type=ValueError,
497
+ reference=self._reference,
498
+ )
499
+ )
500
+
501
+ return self
502
+
503
+
504
+ class SecuritySchemeObject(OAS30SecuritySchemeObject):
505
+ """
506
+ Validates the OpenAPI Security Scheme object - §4.8.27
507
+ """
508
+
509
+ _SECURITY_SCHEME_TYPES: ClassVar[set[str]] = {
510
+ "apiKey",
511
+ "http",
512
+ "oauth2",
513
+ "openIdConnect",
514
+ "mutualTLS",
515
+ }
516
+
517
+
518
+ type _Requirement = dict[str, list[str]]
519
+
520
+
521
+ # NB This is implemented as a RootModel as there are no pre-defined field names.
522
+ class SecurityRequirementObject(RootModel[list[_Requirement] | _Requirement]):
523
+ """
524
+ Validates the OpenAPI Specification security requirement object - §4.8.30:
525
+ """
526
+
527
+ # FIXME: The name must be a valid Security Scheme - need to use post-processing
528
+ # FIXME If the security scheme is of type "oauth2" or "openIdConnect", then the
529
+ # value must be a list
530
+ _reference: ClassVar[Reference] = Reference(
531
+ title=TITLE,
532
+ url="https://spec.openapis.org/oas/3.1.1.html#security-requirement-object",
533
+ section="Security Requirement Object",
534
+ )
535
+
536
+
537
+ @specification_extensions("x-")
538
+ class ComponentsObject(GenericObject):
539
+ """
540
+ Validates the OpenAPI Specification components object - §4.8.7
541
+ """
542
+
543
+ schemas: Optional[dict[str, SchemaObject | ReferenceObject]] = None
544
+ responses: Optional[dict[str, ResponseObject | ReferenceObject]] = None
545
+ paremeters: Optional[dict[str, ParameterObject | ReferenceObject]] = None
546
+ examples: Optional[dict[str, ExampleObject | ReferenceObject]] = None
547
+ requestBodies: Optional[dict[str, RequestBodyObject | ReferenceObject]] = None
548
+ headers: Optional[dict[str, HeaderObject | ReferenceObject]] = None
549
+ securitySchemes: Optional[dict[str, SecuritySchemeObject | ReferenceObject]] = None
550
+ links: Optional[dict[str, LinkObject | ReferenceObject]] = None
551
+ callbacks: Optional[dict[str, CallbackObject | ReferenceObject]] = None
552
+ pathItems: Optional[dict[str, PathItemObject]] = None
553
+ _reference: ClassVar[Reference] = Reference(
554
+ title=TITLE,
555
+ url="https://spec.openapis.org/oas/v3.1.1.html#components-object",
556
+ section="Components Object",
557
+ )
558
+
559
+ @model_validator(mode="before")
560
+ @classmethod
561
+ def validate_all_fields(
562
+ cls, data: dict[str, dict[str, Any]]
563
+ ) -> dict[str, dict[str, Any]]:
564
+ """
565
+ Validates the components object.
566
+
567
+ Args:
568
+ data: The data to validate.
569
+
570
+ Returns:
571
+ The validated components object.
572
+ """
573
+
574
+ pattern: str = r"^[a-zA-Z0-9\.\-_]+$"
575
+
576
+ # Validate each field in the components object
577
+ for field_name, value in data.items():
578
+ if field_name.startswith("x-"):
579
+ continue
580
+
581
+ if not isinstance(value, dict): # type: ignore
582
+ raise ValueError(
583
+ f"Invalid type for '{field_name}': expected dict, got {type(value)}"
584
+ )
585
+
586
+ for key in value.keys():
587
+ if not re.match(pattern, key):
588
+ raise ValueError(
589
+ f"Invalid key '{key}' in '{field_name}': must match pattern {pattern}" # pylint: disable=line-too-long
590
+ )
591
+
592
+ return data
593
+
594
+
595
+ @specification_extensions("x-")
596
+ class OpenAPIObject(GenericObject):
597
+ """
598
+ Validates the OpenAPI Specification object - §4.1
599
+ """
600
+
601
+ openapi: OpenAPI
602
+ info: InfoObject
603
+ jsonSchemaDialect: Optional[URI] = None
604
+ servers: Optional[list[ServerObject]] = Field(default=[ServerObject(url=URI("/"))])
605
+ paths: Optional[PathsObject] = None
606
+ webhooks: Optional[dict[str, PathItemObject]] = None
607
+ components: Optional[ComponentsObject] = None
608
+ security: Optional[list[SecurityRequirementObject]] = None
609
+ tags: Optional[list[TagObject]] = None
610
+ externalDocs: Optional[ExternalDocumentationObject] = None
611
+ _reference: ClassVar[Reference] = Reference(
612
+ title=TITLE,
613
+ url="https://spec.openapis.org/oas/3.1.1.html#openapi-object",
614
+ section="OpenAPI Object",
615
+ )
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.4
2
+ Name: amati
3
+ Version: 0.1.0
4
+ Summary: Validates that a .yaml or .json file conforms to the OpenAPI Specifications 3.x. This is an ALPHA build.
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: jsonpickle>=4.1.1
19
+ Requires-Dist: jsonschema>=4.24.0
20
+ Requires-Dist: pydantic>=2.11.5
21
+ Requires-Dist: pyyaml>=6.0.2
22
+ Description-Content-Type: text/markdown
23
+
24
+ # amati
25
+
26
+ A programme designed to validate that a file conforms to [OpenAPI Specification](https://spec.openapis.org/oas/v3.1.1.html) (OAS).
27
+
28
+ Currently a proof of concept.
29
+
30
+ ## Name
31
+
32
+ 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
+
34
+ ## Architecture
35
+
36
+ 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
+
38
+ 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
+
40
+ ## Requirements
41
+
42
+ * The latest version of [uv](https://docs.astral.sh/uv/)
43
+ * [git 2.49+](https://git-scm.com/downloads/linux)
44
+
45
+ ## Testing and formatting
46
+
47
+ This project uses:
48
+
49
+ * [Pytest](https://docs.pytest.org/en/stable/) as a testing framework
50
+ * [PyLance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) on strict mode for type checking
51
+ * [Pylint](https://www.pylint.org/) as a linter, using a modified version from [Google's style guide](https://google.github.io/styleguide/pyguide.html)
52
+ * [Hypothesis](https://hypothesis.readthedocs.io/en/latest/index.html) for test data generation
53
+ * [Coverage](https://coverage.readthedocs.io/en/7.6.8/) on both the tests and code for test coverage
54
+ * [Black](https://black.readthedocs.io/en/stable/index.html) for automated formatting
55
+ * [isort](https://pycqa.github.io/isort/) for import sorting
56
+
57
+ It's expected that there are no errors, no surviving mutants and 100% of the code is reached and executed.
58
+
59
+ 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
+
61
+ ```sh
62
+ python scripts/tests/setup_test_specs.py
63
+ ```
64
+
65
+ To run everything, from linting, type checking to downloading test specs run:
66
+
67
+ ```sh
68
+ sh bin/checks.sh
69
+ ```
70
+
71
+ ## Building
72
+
73
+ 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
+
75
+ To install, assuming that [uv](https://docs.astral.sh/uv/) is already installed and initialised
76
+
77
+ ```sh
78
+ uv python install
79
+ uv venv
80
+ uv sync
81
+ ```
82
+
83
+ ### Data
84
+
85
+ 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`.
86
+
87
+
88
+
89
+