diff --git a/.github/workflows/validate-models.yml b/.github/workflows/validate-models.yml new file mode 100644 index 0000000..1bed151 --- /dev/null +++ b/.github/workflows/validate-models.yml @@ -0,0 +1,51 @@ +name: Validate API Compliance + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +permissions: + contents: read + +jobs: + check-models: + runs-on: ubuntu-latest + + env: + OPENAPI_URL: https://raw.githubusercontent.com/CIRFMF/ksef-docs/main/open-api.json + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Download official open-api.json + run: | + curl -L -o open-api.json "${{ env.OPENAPI_URL }}" + + - name: Generate models + run: | + # Use the downloaded open-api.json as input + python tools/generate_openapi_models.py --input open-api.json --output src/ksef_client/openapi_models.py + + - name: Verify no changes in models + run: | + # Check if the generated file differs from the committed file + if ! git diff --exit-code src/ksef_client/openapi_models.py; then + echo "::group::Diff" + git diff src/ksef_client/openapi_models.py + echo "::endgroup::" + echo "::error::Generated models do not match the official open-api.json from ${{ env.OPENAPI_URL }}. Please run generation locally with the latest spec and commit changes." + exit 1 + fi + + - name: Check API Coverage + run: | + # Verify that all OpenAPI endpoints are implemented in the client code + python tools/check_coverage.py --openapi open-api.json --src src/ksef_client/clients diff --git a/src/ksef_client/clients/certificates.py b/src/ksef_client/clients/certificates.py index ba9c20c..1e90af7 100644 --- a/src/ksef_client/clients/certificates.py +++ b/src/ksef_client/clients/certificates.py @@ -67,6 +67,7 @@ def revoke_certificate( f"/certificates/{certificate_serial_number}/revoke", json=request_payload, access_token=access_token, + expected_status={204}, ) @@ -134,4 +135,5 @@ async def revoke_certificate( f"/certificates/{certificate_serial_number}/revoke", json=request_payload, access_token=access_token, + expected_status={204}, ) diff --git a/src/ksef_client/clients/security.py b/src/ksef_client/clients/security.py index fb280ad..ba72560 100644 --- a/src/ksef_client/clients/security.py +++ b/src/ksef_client/clients/security.py @@ -9,27 +9,7 @@ class SecurityClient(BaseApiClient): def get_public_key_certificates(self) -> Any: return self._request_json("GET", "/security/public-key-certificates", skip_auth=True) - def get_public_key_pem(self) -> str: - content = self._request_bytes( - "GET", - "/public-keys/publicKey.pem", - headers={"Accept": "application/x-pem-file"}, - skip_auth=True, - expected_status={200}, - ) - return content.decode("utf-8") - class AsyncSecurityClient(AsyncBaseApiClient): async def get_public_key_certificates(self) -> Any: return await self._request_json("GET", "/security/public-key-certificates", skip_auth=True) - - async def get_public_key_pem(self) -> str: - content = await self._request_bytes( - "GET", - "/public-keys/publicKey.pem", - headers={"Accept": "application/x-pem-file"}, - skip_auth=True, - expected_status={200}, - ) - return content.decode("utf-8") diff --git a/src/ksef_client/openapi_models.py b/src/ksef_client/openapi_models.py index a15b1e0..e0a7879 100644 --- a/src/ksef_client/openapi_models.py +++ b/src/ksef_client/openapi_models.py @@ -1,17 +1,18 @@ +# ruff: noqa # Generated from ksef-docs/open-api.json. Do not edit manually. from __future__ import annotations -import sys -from dataclasses import dataclass, field, fields +from dataclasses import MISSING, dataclass, field, fields from enum import Enum -from typing import Any, TypeAlias, TypeVar, cast, get_args, get_origin, get_type_hints +import sys +from typing import Any, Optional, TypeAlias, TypeVar +from typing import get_args, get_origin, get_type_hints JsonValue: TypeAlias = Any T = TypeVar("T", bound="OpenApiModel") _TYPE_CACHE: dict[type, dict[str, Any]] = {} - def _get_type_map(cls: type) -> dict[str, Any]: cached = _TYPE_CACHE.get(cls) if cached is not None: @@ -21,7 +22,6 @@ def _get_type_map(cls: type) -> dict[str, Any]: _TYPE_CACHE[cls] = hints return hints - def _convert_value(type_hint: Any, value: Any) -> Any: if value is None: return None @@ -41,15 +41,11 @@ def _convert_value(type_hint: Any, value: Any) -> Any: return _convert_value(args[0], value) if isinstance(type_hint, type) and issubclass(type_hint, Enum): return type_hint(value) - if ( - isinstance(type_hint, type) - and issubclass(type_hint, OpenApiModel) - and isinstance(value, dict) - ): - return type_hint.from_dict(value) + if isinstance(type_hint, type) and issubclass(type_hint, OpenApiModel): + if isinstance(value, dict): + return type_hint.from_dict(value) return value - def _serialize_value(value: Any) -> Any: if isinstance(value, Enum): return value.value @@ -61,7 +57,6 @@ def _serialize_value(value: Any) -> Any: return {key: _serialize_value(item) for key, item in value.items()} return value - class OpenApiModel: @classmethod def from_dict(cls: type[T], data: dict[str, Any]) -> T: @@ -69,7 +64,7 @@ def from_dict(cls: type[T], data: dict[str, Any]) -> T: raise ValueError("data is None") type_map = _get_type_map(cls) kwargs: dict[str, Any] = {} - for model_field in fields(cast(Any, cls)): + for model_field in fields(cls): # type: ignore json_key = model_field.metadata.get("json_key", model_field.name) if json_key in data: type_hint = type_map.get(model_field.name, Any) @@ -78,7 +73,7 @@ def from_dict(cls: type[T], data: dict[str, Any]) -> T: def to_dict(self, omit_none: bool = True) -> dict[str, Any]: result: dict[str, Any] = {} - for model_field in fields(cast(Any, self)): + for model_field in fields(self): # type: ignore json_key = model_field.metadata.get("json_key", model_field.name) value = getattr(self, model_field.name) if omit_none and value is None: @@ -86,20 +81,17 @@ def to_dict(self, omit_none: bool = True) -> dict[str, Any]: result[json_key] = _serialize_value(value) return result - class AmountType(Enum): BRUTTO = "Brutto" NETTO = "Netto" VAT = "Vat" - class AuthenticationContextIdentifierType(Enum): NIP = "Nip" INTERNALID = "InternalId" NIPVATUE = "NipVatUe" PEPPOLID = "PeppolId" - class AuthenticationMethod(Enum): TOKEN = "Token" TRUSTEDPROFILE = "TrustedProfile" @@ -109,7 +101,6 @@ class AuthenticationMethod(Enum): PERSONALSIGNATURE = "PersonalSignature" PEPPOLSIGNATURE = "PeppolSignature" - class AuthenticationTokenStatus(Enum): PENDING = "Pending" ACTIVE = "Active" @@ -117,40 +108,34 @@ class AuthenticationTokenStatus(Enum): REVOKED = "Revoked" FAILED = "Failed" - class BuyerIdentifierType(Enum): NIP = "Nip" VATUE = "VatUe" OTHER = "Other" NONE = "None" - class CertificateListItemStatus(Enum): ACTIVE = "Active" BLOCKED = "Blocked" REVOKED = "Revoked" EXPIRED = "Expired" - class CertificateRevocationReason(Enum): UNSPECIFIED = "Unspecified" SUPERSEDED = "Superseded" KEYCOMPROMISE = "KeyCompromise" - class CertificateSubjectIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" - class CommonSessionStatus(Enum): INPROGRESS = "InProgress" SUCCEEDED = "Succeeded" FAILED = "Failed" CANCELLED = "Cancelled" - class CurrencyCode(Enum): AED = "AED" AFN = "AFN" @@ -335,47 +320,38 @@ class CurrencyCode(Enum): ZMW = "ZMW" ZWL = "ZWL" - class EntityAuthorizationPermissionType(Enum): SELFINVOICING = "SelfInvoicing" RRINVOICING = "RRInvoicing" TAXREPRESENTATIVE = "TaxRepresentative" PEFINVOICING = "PefInvoicing" - class EntityAuthorizationPermissionsSubjectIdentifierType(Enum): NIP = "Nip" PEPPOLID = "PeppolId" - class EntityAuthorizationsAuthorIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" - class EntityAuthorizationsAuthorizedEntityIdentifierType(Enum): NIP = "Nip" PEPPOLID = "PeppolId" - class EntityAuthorizationsAuthorizingEntityIdentifierType(Enum): NIP = "Nip" - class EntityPermissionType(Enum): INVOICEWRITE = "InvoiceWrite" INVOICEREAD = "InvoiceRead" - class EntityPermissionsSubjectIdentifierType(Enum): NIP = "Nip" - class EntityPermissionsSubordinateEntityIdentifierType(Enum): NIP = "Nip" - class EntityRoleType(Enum): COURTBAILIFF = "CourtBailiff" ENFORCEMENTAUTHORITY = "EnforcementAuthority" @@ -384,103 +360,84 @@ class EntityRoleType(Enum): VATGROUPUNIT = "VatGroupUnit" VATGROUPSUBUNIT = "VatGroupSubUnit" - class EntityRolesParentEntityIdentifierType(Enum): NIP = "Nip" - class EntitySubjectByFingerprintDetailsType(Enum): ENTITYBYFINGERPRINT = "EntityByFingerprint" - class EntitySubjectByIdentifierDetailsType(Enum): ENTITYBYIDENTIFIER = "EntityByIdentifier" - class EntitySubjectDetailsType(Enum): ENTITYBYIDENTIFIER = "EntityByIdentifier" ENTITYBYFINGERPRINT = "EntityByFingerprint" - class EuEntityAdministrationPermissionsContextIdentifierType(Enum): NIPVATUE = "NipVatUe" - class EuEntityAdministrationPermissionsSubjectIdentifierType(Enum): FINGERPRINT = "Fingerprint" - class EuEntityPermissionSubjectDetailsType(Enum): PERSONBYFINGERPRINTWITHIDENTIFIER = "PersonByFingerprintWithIdentifier" PERSONBYFINGERPRINTWITHOUTIDENTIFIER = "PersonByFingerprintWithoutIdentifier" ENTITYBYFINGERPRINT = "EntityByFingerprint" - class EuEntityPermissionType(Enum): INVOICEWRITE = "InvoiceWrite" INVOICEREAD = "InvoiceRead" - class EuEntityPermissionsAuthorIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" - class EuEntityPermissionsQueryPermissionType(Enum): VATUEMANAGE = "VatUeManage" INVOICEWRITE = "InvoiceWrite" INVOICEREAD = "InvoiceRead" INTROSPECTION = "Introspection" - class EuEntityPermissionsSubjectIdentifierType(Enum): FINGERPRINT = "Fingerprint" - class IndirectPermissionType(Enum): INVOICEREAD = "InvoiceRead" INVOICEWRITE = "InvoiceWrite" - class IndirectPermissionsSubjectIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" - class IndirectPermissionsTargetIdentifierType(Enum): NIP = "Nip" ALLPARTNERS = "AllPartners" INTERNALID = "InternalId" - class InvoicePermissionType(Enum): SELFINVOICING = "SelfInvoicing" TAXREPRESENTATIVE = "TaxRepresentative" RRINVOICING = "RRInvoicing" PEFINVOICING = "PefInvoicing" - class InvoiceQueryDateType(Enum): ISSUE = "Issue" INVOICING = "Invoicing" PERMANENTSTORAGE = "PermanentStorage" - class InvoiceQueryFormType(Enum): FA = "FA" PEF = "PEF" RR = "RR" - class InvoiceQuerySubjectType(Enum): SUBJECT1 = "Subject1" SUBJECT2 = "Subject2" SUBJECT3 = "Subject3" SUBJECTAUTHORIZED = "SubjectAuthorized" - class InvoiceType(Enum): VAT = "Vat" ZAL = "Zal" @@ -495,27 +452,22 @@ class InvoiceType(Enum): VATRR = "VatRr" KORVATRR = "KorVatRr" - class InvoicingMode(Enum): ONLINE = "Online" OFFLINE = "Offline" - class KsefCertificateType(Enum): AUTHENTICATION = "Authentication" OFFLINE = "Offline" - class PermissionState(Enum): ACTIVE = "Active" INACTIVE = "Inactive" - class PersonIdentifierType(Enum): PESEL = "Pesel" NIP = "Nip" - class PersonPermissionScope(Enum): CREDENTIALSMANAGE = "CredentialsManage" CREDENTIALSREAD = "CredentialsRead" @@ -525,13 +477,11 @@ class PersonPermissionScope(Enum): SUBUNITMANAGE = "SubunitManage" ENFORCEMENTOPERATIONS = "EnforcementOperations" - class PersonPermissionSubjectDetailsType(Enum): PERSONBYIDENTIFIER = "PersonByIdentifier" PERSONBYFINGERPRINTWITHIDENTIFIER = "PersonByFingerprintWithIdentifier" PERSONBYFINGERPRINTWITHOUTIDENTIFIER = "PersonByFingerprintWithoutIdentifier" - class PersonPermissionType(Enum): CREDENTIALSMANAGE = "CredentialsManage" CREDENTIALSREAD = "CredentialsRead" @@ -541,53 +491,44 @@ class PersonPermissionType(Enum): SUBUNITMANAGE = "SubunitManage" ENFORCEMENTOPERATIONS = "EnforcementOperations" - class PersonPermissionsAuthorIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" SYSTEM = "System" - class PersonPermissionsAuthorizedIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" - class PersonPermissionsContextIdentifierType(Enum): NIP = "Nip" INTERNALID = "InternalId" - class PersonPermissionsQueryType(Enum): PERMISSIONSINCURRENTCONTEXT = "PermissionsInCurrentContext" PERMISSIONSGRANTEDINCURRENTCONTEXT = "PermissionsGrantedInCurrentContext" - class PersonPermissionsSubjectIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" - class PersonPermissionsTargetIdentifierType(Enum): NIP = "Nip" ALLPARTNERS = "AllPartners" INTERNALID = "InternalId" - class PersonSubjectByFingerprintDetailsType(Enum): PERSONBYFINGERPRINTWITHIDENTIFIER = "PersonByFingerprintWithIdentifier" PERSONBYFINGERPRINTWITHOUTIDENTIFIER = "PersonByFingerprintWithoutIdentifier" - class PersonSubjectDetailsType(Enum): PERSONBYIDENTIFIER = "PersonByIdentifier" PERSONBYFINGERPRINTWITHIDENTIFIER = "PersonByFingerprintWithIdentifier" PERSONBYFINGERPRINTWITHOUTIDENTIFIER = "PersonByFingerprintWithoutIdentifier" - class PersonalPermissionScope(Enum): CREDENTIALSMANAGE = "CredentialsManage" CREDENTIALSREAD = "CredentialsRead" @@ -598,7 +539,6 @@ class PersonalPermissionScope(Enum): ENFORCEMENTOPERATIONS = "EnforcementOperations" VATUEMANAGE = "VatUeManage" - class PersonalPermissionType(Enum): CREDENTIALSMANAGE = "CredentialsManage" CREDENTIALSREAD = "CredentialsRead" @@ -609,101 +549,82 @@ class PersonalPermissionType(Enum): ENFORCEMENTOPERATIONS = "EnforcementOperations" VATUEMANAGE = "VatUeManage" - class PersonalPermissionsAuthorizedIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" - class PersonalPermissionsContextIdentifierType(Enum): NIP = "Nip" INTERNALID = "InternalId" - class PersonalPermissionsTargetIdentifierType(Enum): NIP = "Nip" ALLPARTNERS = "AllPartners" INTERNALID = "InternalId" - class PublicKeyCertificateUsage(Enum): KSEFTOKENENCRYPTION = "KsefTokenEncryption" SYMMETRICKEYENCRYPTION = "SymmetricKeyEncryption" - class QueryType(Enum): GRANTED = "Granted" RECEIVED = "Received" - class SessionType(Enum): ONLINE = "Online" BATCH = "Batch" - class SortOrder(Enum): ASC = "Asc" DESC = "Desc" - class SubjectIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" - class SubjectType(Enum): ENFORCEMENTAUTHORITY = "EnforcementAuthority" VATGROUP = "VatGroup" JST = "JST" - class SubordinateEntityRoleType(Enum): LOCALGOVERNMENTSUBUNIT = "LocalGovernmentSubUnit" VATGROUPSUBUNIT = "VatGroupSubUnit" - class SubordinateRoleSubordinateEntityIdentifierType(Enum): NIP = "Nip" - class SubunitPermissionScope(Enum): CREDENTIALSMANAGE = "CredentialsManage" - class SubunitPermissionsAuthorIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" - class SubunitPermissionsContextIdentifierType(Enum): INTERNALID = "InternalId" NIP = "Nip" - class SubunitPermissionsSubjectIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" - class SubunitPermissionsSubunitIdentifierType(Enum): INTERNALID = "InternalId" NIP = "Nip" - class TestDataAuthorizedIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" - class TestDataContextIdentifierType(Enum): NIP = "Nip" - class TestDataPermissionType(Enum): INVOICEREAD = "InvoiceRead" INVOICEWRITE = "InvoiceWrite" @@ -713,7 +634,6 @@ class TestDataPermissionType(Enum): ENFORCEMENTOPERATIONS = "EnforcementOperations" SUBUNITMANAGE = "SubunitManage" - class ThirdSubjectIdentifierType(Enum): NIP = "Nip" INTERNALID = "InternalId" @@ -721,20 +641,17 @@ class ThirdSubjectIdentifierType(Enum): OTHER = "Other" NONE = "None" - class TokenAuthorIdentifierType(Enum): NIP = "Nip" PESEL = "Pesel" FINGERPRINT = "Fingerprint" - class TokenContextIdentifierType(Enum): NIP = "Nip" INTERNALID = "InternalId" NIPVATUE = "NipVatUe" PEPPOLID = "PeppolId" - class TokenPermissionType(Enum): INVOICEREAD = "InvoiceRead" INVOICEWRITE = "InvoiceWrite" @@ -743,7 +660,6 @@ class TokenPermissionType(Enum): SUBUNITMANAGE = "SubunitManage" ENFORCEMENTOPERATIONS = "EnforcementOperations" - Challenge: TypeAlias = str InternalId: TypeAlias = str @@ -766,13 +682,11 @@ class TokenPermissionType(Enum): Sha256HashBase64: TypeAlias = str - @dataclass(frozen=True) class AllowedIps(OpenApiModel): - ip4Addresses: list[str] | None = None - ip4Masks: list[str] | None = None - ip4Ranges: list[str] | None = None - + ip4Addresses: Optional[list[str]] = None + ip4Masks: Optional[list[str]] = None + ip4Ranges: Optional[list[str]] = None @dataclass(frozen=True) class ApiRateLimitValuesOverride(OpenApiModel): @@ -780,7 +694,6 @@ class ApiRateLimitValuesOverride(OpenApiModel): perMinute: int perSecond: int - @dataclass(frozen=True) class ApiRateLimitsOverride(OpenApiModel): batchSession: ApiRateLimitValuesOverride @@ -796,17 +709,14 @@ class ApiRateLimitsOverride(OpenApiModel): sessionList: ApiRateLimitValuesOverride sessionMisc: ApiRateLimitValuesOverride - @dataclass(frozen=True) class AttachmentPermissionGrantRequest(OpenApiModel): nip: Nip - @dataclass(frozen=True) class AttachmentPermissionRevokeRequest(OpenApiModel): nip: Nip - expectedEndDate: str | None = None - + expectedEndDate: Optional[str] = None @dataclass(frozen=True) class AuthenticationChallengeResponse(OpenApiModel): @@ -814,62 +724,53 @@ class AuthenticationChallengeResponse(OpenApiModel): timestamp: str timestampMs: int - @dataclass(frozen=True) class AuthenticationContextIdentifier(OpenApiModel): type: AuthenticationContextIdentifierType value: str - @dataclass(frozen=True) class AuthenticationInitResponse(OpenApiModel): authenticationToken: TokenInfo referenceNumber: ReferenceNumber - @dataclass(frozen=True) class AuthenticationListItem(OpenApiModel): authenticationMethod: AuthenticationMethod referenceNumber: ReferenceNumber startDate: str status: StatusInfo - isCurrent: bool | None = None - isTokenRedeemed: bool | None = None - lastTokenRefreshDate: str | None = None - refreshTokenValidUntil: str | None = None - + isCurrent: Optional[bool] = None + isTokenRedeemed: Optional[bool] = None + lastTokenRefreshDate: Optional[str] = None + refreshTokenValidUntil: Optional[str] = None @dataclass(frozen=True) class AuthenticationListResponse(OpenApiModel): items: list[AuthenticationListItem] - continuationToken: str | None = None - + continuationToken: Optional[str] = None @dataclass(frozen=True) class AuthenticationOperationStatusResponse(OpenApiModel): authenticationMethod: AuthenticationMethod startDate: str status: StatusInfo - isTokenRedeemed: bool | None = None - lastTokenRefreshDate: str | None = None - refreshTokenValidUntil: str | None = None - + isTokenRedeemed: Optional[bool] = None + lastTokenRefreshDate: Optional[str] = None + refreshTokenValidUntil: Optional[str] = None @dataclass(frozen=True) class AuthenticationTokenRefreshResponse(OpenApiModel): accessToken: TokenInfo - @dataclass(frozen=True) class AuthenticationTokensResponse(OpenApiModel): accessToken: TokenInfo refreshToken: TokenInfo - @dataclass(frozen=True) class AuthorizationPolicy(OpenApiModel): - allowedIps: AllowedIps | None = None - + allowedIps: Optional[AllowedIps] = None @dataclass(frozen=True) class BatchFileInfo(OpenApiModel): @@ -877,65 +778,56 @@ class BatchFileInfo(OpenApiModel): fileParts: list[BatchFilePartInfo] fileSize: int - @dataclass(frozen=True) class BatchFilePartInfo(OpenApiModel): fileHash: Sha256HashBase64 fileSize: int ordinalNumber: int - @dataclass(frozen=True) class BatchSessionContextLimitsOverride(OpenApiModel): maxInvoiceSizeInMB: int maxInvoiceWithAttachmentSizeInMB: int maxInvoices: int - @dataclass(frozen=True) class BatchSessionEffectiveContextLimits(OpenApiModel): maxInvoiceSizeInMB: int maxInvoiceWithAttachmentSizeInMB: int maxInvoices: int - @dataclass(frozen=True) class CertificateEffectiveSubjectLimits(OpenApiModel): - maxCertificates: int | None = None - + maxCertificates: Optional[int] = None @dataclass(frozen=True) class CertificateEnrollmentDataResponse(OpenApiModel): commonName: str countryName: str - givenName: str | None = None - organizationIdentifier: str | None = None - organizationName: str | None = None - serialNumber: str | None = None - surname: str | None = None - uniqueIdentifier: str | None = None - + givenName: Optional[str] = None + organizationIdentifier: Optional[str] = None + organizationName: Optional[str] = None + serialNumber: Optional[str] = None + surname: Optional[str] = None + uniqueIdentifier: Optional[str] = None @dataclass(frozen=True) class CertificateEnrollmentStatusResponse(OpenApiModel): requestDate: str status: StatusInfo - certificateSerialNumber: str | None = None - + certificateSerialNumber: Optional[str] = None @dataclass(frozen=True) class CertificateLimit(OpenApiModel): limit: int remaining: int - @dataclass(frozen=True) class CertificateLimitsResponse(OpenApiModel): canRequest: bool certificate: CertificateLimit enrollment: CertificateLimit - @dataclass(frozen=True) class CertificateListItem(OpenApiModel): certificateSerialNumber: str @@ -947,25 +839,21 @@ class CertificateListItem(OpenApiModel): type: KsefCertificateType validFrom: str validTo: str - lastUseDate: str | None = None - + lastUseDate: Optional[str] = None @dataclass(frozen=True) class CertificateSubjectIdentifier(OpenApiModel): type: CertificateSubjectIdentifierType value: str - @dataclass(frozen=True) class CertificateSubjectLimitsOverride(OpenApiModel): - maxCertificates: int | None = None - + maxCertificates: Optional[int] = None @dataclass(frozen=True) class CheckAttachmentPermissionStatusResponse(OpenApiModel): - isAttachmentAllowed: bool | None = None - revokedDate: str | None = None - + isAttachmentAllowed: Optional[bool] = None + revokedDate: Optional[str] = None @dataclass(frozen=True) class EffectiveApiRateLimitValues(OpenApiModel): @@ -973,7 +861,6 @@ class EffectiveApiRateLimitValues(OpenApiModel): perMinute: int perSecond: int - @dataclass(frozen=True) class EffectiveApiRateLimits(OpenApiModel): batchSession: EffectiveApiRateLimitValues @@ -989,48 +876,40 @@ class EffectiveApiRateLimits(OpenApiModel): sessionList: EffectiveApiRateLimitValues sessionMisc: EffectiveApiRateLimitValues - @dataclass(frozen=True) class EffectiveContextLimits(OpenApiModel): batchSession: BatchSessionEffectiveContextLimits onlineSession: OnlineSessionEffectiveContextLimits - @dataclass(frozen=True) class EffectiveSubjectLimits(OpenApiModel): - certificate: CertificateEffectiveSubjectLimits | None = None - enrollment: EnrollmentEffectiveSubjectLimits | None = None - + certificate: Optional[CertificateEffectiveSubjectLimits] = None + enrollment: Optional[EnrollmentEffectiveSubjectLimits] = None @dataclass(frozen=True) class EncryptionInfo(OpenApiModel): encryptedSymmetricKey: str initializationVector: str - @dataclass(frozen=True) class EnrollCertificateRequest(OpenApiModel): certificateName: str certificateType: KsefCertificateType csr: str - validFrom: str | None = None - + validFrom: Optional[str] = None @dataclass(frozen=True) class EnrollCertificateResponse(OpenApiModel): referenceNumber: ReferenceNumber timestamp: str - @dataclass(frozen=True) class EnrollmentEffectiveSubjectLimits(OpenApiModel): - maxEnrollments: int | None = None - + maxEnrollments: Optional[int] = None @dataclass(frozen=True) class EnrollmentSubjectLimitsOverride(OpenApiModel): - maxEnrollments: int | None = None - + maxEnrollments: Optional[int] = None @dataclass(frozen=True) class EntityAuthorizationGrant(OpenApiModel): @@ -1040,9 +919,8 @@ class EntityAuthorizationGrant(OpenApiModel): description: str id: PermissionId startDate: str - authorIdentifier: EntityAuthorizationsAuthorIdentifier | None = None - subjectEntityDetails: PermissionsSubjectEntityByIdentifierDetails | None = None - + authorIdentifier: Optional[EntityAuthorizationsAuthorIdentifier] = None + subjectEntityDetails: Optional[PermissionsSubjectEntityByIdentifierDetails] = None @dataclass(frozen=True) class EntityAuthorizationPermissionsGrantRequest(OpenApiModel): @@ -1051,55 +929,46 @@ class EntityAuthorizationPermissionsGrantRequest(OpenApiModel): subjectDetails: EntityDetails subjectIdentifier: EntityAuthorizationPermissionsSubjectIdentifier - @dataclass(frozen=True) class EntityAuthorizationPermissionsQueryRequest(OpenApiModel): queryType: QueryType - authorizedIdentifier: EntityAuthorizationsAuthorizedEntityIdentifier | None = None - authorizingIdentifier: EntityAuthorizationsAuthorizingEntityIdentifier | None = None - permissionTypes: list[InvoicePermissionType] | None = None - + authorizedIdentifier: Optional[EntityAuthorizationsAuthorizedEntityIdentifier] = None + authorizingIdentifier: Optional[EntityAuthorizationsAuthorizingEntityIdentifier] = None + permissionTypes: Optional[list[InvoicePermissionType]] = None @dataclass(frozen=True) class EntityAuthorizationPermissionsSubjectIdentifier(OpenApiModel): type: EntityAuthorizationPermissionsSubjectIdentifierType value: str - @dataclass(frozen=True) class EntityAuthorizationsAuthorIdentifier(OpenApiModel): type: EntityAuthorizationsAuthorIdentifierType value: str - @dataclass(frozen=True) class EntityAuthorizationsAuthorizedEntityIdentifier(OpenApiModel): type: EntityAuthorizationsAuthorizedEntityIdentifierType value: str - @dataclass(frozen=True) class EntityAuthorizationsAuthorizingEntityIdentifier(OpenApiModel): type: EntityAuthorizationsAuthorizingEntityIdentifierType value: str - @dataclass(frozen=True) class EntityByFingerprintDetails(OpenApiModel): address: str fullName: str - @dataclass(frozen=True) class EntityDetails(OpenApiModel): fullName: str - @dataclass(frozen=True) class EntityPermission(OpenApiModel): type: EntityPermissionType - canDelegate: bool | None = None - + canDelegate: Optional[bool] = None @dataclass(frozen=True) class EntityPermissionsGrantRequest(OpenApiModel): @@ -1108,39 +977,33 @@ class EntityPermissionsGrantRequest(OpenApiModel): subjectDetails: EntityDetails subjectIdentifier: EntityPermissionsSubjectIdentifier - @dataclass(frozen=True) class EntityPermissionsSubjectIdentifier(OpenApiModel): type: EntityPermissionsSubjectIdentifierType value: str - @dataclass(frozen=True) class EntityPermissionsSubordinateEntityIdentifier(OpenApiModel): type: EntityPermissionsSubordinateEntityIdentifierType value: str - @dataclass(frozen=True) class EntityRole(OpenApiModel): description: str role: EntityRoleType startDate: str - parentEntityIdentifier: EntityRolesParentEntityIdentifier | None = None - + parentEntityIdentifier: Optional[EntityRolesParentEntityIdentifier] = None @dataclass(frozen=True) class EntityRolesParentEntityIdentifier(OpenApiModel): type: EntityRolesParentEntityIdentifierType value: str - @dataclass(frozen=True) class EuEntityAdministrationPermissionsContextIdentifier(OpenApiModel): type: EuEntityAdministrationPermissionsContextIdentifierType value: str - @dataclass(frozen=True) class EuEntityAdministrationPermissionsGrantRequest(OpenApiModel): contextIdentifier: EuEntityAdministrationPermissionsContextIdentifier @@ -1150,19 +1013,16 @@ class EuEntityAdministrationPermissionsGrantRequest(OpenApiModel): subjectDetails: EuEntityPermissionSubjectDetails subjectIdentifier: EuEntityAdministrationPermissionsSubjectIdentifier - @dataclass(frozen=True) class EuEntityAdministrationPermissionsSubjectIdentifier(OpenApiModel): type: EuEntityAdministrationPermissionsSubjectIdentifierType value: str - @dataclass(frozen=True) class EuEntityDetails(OpenApiModel): address: str fullName: str - @dataclass(frozen=True) class EuEntityPermission(OpenApiModel): authorIdentifier: EuEntityPermissionsAuthorIdentifier @@ -1173,25 +1033,22 @@ class EuEntityPermission(OpenApiModel): permissionScope: EuEntityPermissionsQueryPermissionType startDate: str vatUeIdentifier: str - euEntityDetails: PermissionsEuEntityDetails | None = None - subjectEntityDetails: PermissionsSubjectEntityByFingerprintDetails | None = None - subjectPersonDetails: PermissionsSubjectPersonByFingerprintDetails | None = None - + euEntityDetails: Optional[PermissionsEuEntityDetails] = None + subjectEntityDetails: Optional[PermissionsSubjectEntityByFingerprintDetails] = None + subjectPersonDetails: Optional[PermissionsSubjectPersonByFingerprintDetails] = None @dataclass(frozen=True) class EuEntityPermissionSubjectDetails(OpenApiModel): subjectDetailsType: EuEntityPermissionSubjectDetailsType - entityByFp: EntityByFingerprintDetails | None = None - personByFpNoId: PersonByFingerprintWithoutIdentifierDetails | None = None - personByFpWithId: PersonByFingerprintWithIdentifierDetails | None = None - + entityByFp: Optional[EntityByFingerprintDetails] = None + personByFpNoId: Optional[PersonByFingerprintWithoutIdentifierDetails] = None + personByFpWithId: Optional[PersonByFingerprintWithIdentifierDetails] = None @dataclass(frozen=True) class EuEntityPermissionsAuthorIdentifier(OpenApiModel): type: EuEntityPermissionsAuthorIdentifierType value: str - @dataclass(frozen=True) class EuEntityPermissionsGrantRequest(OpenApiModel): description: str @@ -1199,115 +1056,98 @@ class EuEntityPermissionsGrantRequest(OpenApiModel): subjectDetails: EuEntityPermissionSubjectDetails subjectIdentifier: EuEntityPermissionsSubjectIdentifier - @dataclass(frozen=True) class EuEntityPermissionsQueryRequest(OpenApiModel): - authorizedFingerprintIdentifier: str | None = None - permissionTypes: list[EuEntityPermissionsQueryPermissionType] | None = None - vatUeIdentifier: str | None = None - + authorizedFingerprintIdentifier: Optional[str] = None + permissionTypes: Optional[list[EuEntityPermissionsQueryPermissionType]] = None + vatUeIdentifier: Optional[str] = None @dataclass(frozen=True) class EuEntityPermissionsSubjectIdentifier(OpenApiModel): type: EuEntityPermissionsSubjectIdentifierType value: str - @dataclass(frozen=True) class ExceptionDetails(OpenApiModel): - details: list[str] | None = None - exceptionCode: int | None = None - exceptionDescription: str | None = None - + details: Optional[list[str]] = None + exceptionCode: Optional[int] = None + exceptionDescription: Optional[str] = None @dataclass(frozen=True) class ExceptionInfo(OpenApiModel): - exceptionDetailList: list[ExceptionDetails] | None = None - referenceNumber: ReferenceNumber | None = None - serviceCode: str | None = None - serviceCtx: str | None = None - serviceName: str | None = None - timestamp: str | None = None - + exceptionDetailList: Optional[list[ExceptionDetails]] = None + referenceNumber: Optional[ReferenceNumber] = None + serviceCode: Optional[str] = None + serviceCtx: Optional[str] = None + serviceName: Optional[str] = None + timestamp: Optional[str] = None @dataclass(frozen=True) class ExceptionResponse(OpenApiModel): - exception: ExceptionInfo | None = None - + exception: Optional[ExceptionInfo] = None @dataclass(frozen=True) class ExportInvoicesResponse(OpenApiModel): referenceNumber: ReferenceNumber - @dataclass(frozen=True) class FormCode(OpenApiModel): schemaVersion: str systemCode: str value: str - @dataclass(frozen=True) class GenerateTokenRequest(OpenApiModel): description: str permissions: list[TokenPermissionType] - @dataclass(frozen=True) class GenerateTokenResponse(OpenApiModel): referenceNumber: ReferenceNumber token: str - @dataclass(frozen=True) class IdDocument(OpenApiModel): country: str number: str type: str - @dataclass(frozen=True) class IndirectPermissionsGrantRequest(OpenApiModel): description: str permissions: list[IndirectPermissionType] subjectDetails: PersonPermissionSubjectDetails subjectIdentifier: IndirectPermissionsSubjectIdentifier - targetIdentifier: IndirectPermissionsTargetIdentifier | None = None - + targetIdentifier: Optional[IndirectPermissionsTargetIdentifier] = None @dataclass(frozen=True) class IndirectPermissionsSubjectIdentifier(OpenApiModel): type: IndirectPermissionsSubjectIdentifierType value: str - @dataclass(frozen=True) class IndirectPermissionsTargetIdentifier(OpenApiModel): type: IndirectPermissionsTargetIdentifierType - value: str | None = None - + value: Optional[str] = None @dataclass(frozen=True) class InitTokenAuthenticationRequest(OpenApiModel): challenge: Challenge contextIdentifier: AuthenticationContextIdentifier encryptedToken: str - authorizationPolicy: AuthorizationPolicy | None = None - + authorizationPolicy: Optional[AuthorizationPolicy] = None @dataclass(frozen=True) class InvoiceExportRequest(OpenApiModel): encryption: EncryptionInfo filters: InvoiceQueryFilters - @dataclass(frozen=True) class InvoiceExportStatusResponse(OpenApiModel): status: StatusInfo - completedDate: str | None = None - package: InvoicePackage | None = None - packageExpirationDate: str | None = None - + completedDate: Optional[str] = None + package: Optional[InvoicePackage] = None + packageExpirationDate: Optional[str] = None @dataclass(frozen=True) class InvoiceMetadata(OpenApiModel): @@ -1329,48 +1169,41 @@ class InvoiceMetadata(OpenApiModel): permanentStorageDate: str seller: InvoiceMetadataSeller vatAmount: float - authorizedSubject: InvoiceMetadataAuthorizedSubject | None = None - hashOfCorrectedInvoice: Sha256HashBase64 | None = None - thirdSubjects: list[InvoiceMetadataThirdSubject] | None = None - + authorizedSubject: Optional[InvoiceMetadataAuthorizedSubject] = None + hashOfCorrectedInvoice: Optional[Sha256HashBase64] = None + thirdSubjects: Optional[list[InvoiceMetadataThirdSubject]] = None @dataclass(frozen=True) class InvoiceMetadataAuthorizedSubject(OpenApiModel): nip: Nip role: int - name: str | None = None - + name: Optional[str] = None @dataclass(frozen=True) class InvoiceMetadataBuyer(OpenApiModel): identifier: InvoiceMetadataBuyerIdentifier - name: str | None = None - + name: Optional[str] = None @dataclass(frozen=True) class InvoiceMetadataBuyerIdentifier(OpenApiModel): type: BuyerIdentifierType - value: str | None = None - + value: Optional[str] = None @dataclass(frozen=True) class InvoiceMetadataSeller(OpenApiModel): nip: Nip - name: str | None = None - + name: Optional[str] = None @dataclass(frozen=True) class InvoiceMetadataThirdSubject(OpenApiModel): identifier: InvoiceMetadataThirdSubjectIdentifier role: int - name: str | None = None - + name: Optional[str] = None @dataclass(frozen=True) class InvoiceMetadataThirdSubjectIdentifier(OpenApiModel): type: ThirdSubjectIdentifierType - value: str | None = None - + value: Optional[str] = None @dataclass(frozen=True) class InvoicePackage(OpenApiModel): @@ -1378,11 +1211,10 @@ class InvoicePackage(OpenApiModel): isTruncated: bool parts: list[InvoicePackagePart] size: int - lastInvoicingDate: str | None = None - lastIssueDate: str | None = None - lastPermanentStorageDate: str | None = None - permanentStorageHwmDate: str | None = None - + lastInvoicingDate: Optional[str] = None + lastIssueDate: Optional[str] = None + lastPermanentStorageDate: Optional[str] = None + permanentStorageHwmDate: Optional[str] = None @dataclass(frozen=True) class InvoicePackagePart(OpenApiModel): @@ -1396,52 +1228,46 @@ class InvoicePackagePart(OpenApiModel): partSize: int url: str - @dataclass(frozen=True) class InvoiceQueryAmount(OpenApiModel): type: AmountType - from_: float | None = field(default=None, metadata={"json_key": "from"}) - to: float | None = None - + from_: Optional[float] = field(default=None, metadata={"json_key": "from"}) + to: Optional[float] = None @dataclass(frozen=True) class InvoiceQueryBuyerIdentifier(OpenApiModel): type: BuyerIdentifierType - value: str | None = None - + value: Optional[str] = None @dataclass(frozen=True) class InvoiceQueryDateRange(OpenApiModel): dateType: InvoiceQueryDateType from_: str = field(metadata={"json_key": "from"}) - restrictToPermanentStorageHwmDate: bool | None = None - to: str | None = None - + restrictToPermanentStorageHwmDate: Optional[bool] = None + to: Optional[str] = None @dataclass(frozen=True) class InvoiceQueryFilters(OpenApiModel): dateRange: InvoiceQueryDateRange subjectType: InvoiceQuerySubjectType - amount: InvoiceQueryAmount | None = None - buyerIdentifier: InvoiceQueryBuyerIdentifier | None = None - currencyCodes: list[CurrencyCode] | None = None - formType: InvoiceQueryFormType | None = None - hasAttachment: bool | None = None - invoiceNumber: str | None = None - invoiceTypes: list[InvoiceType] | None = None - invoicingMode: InvoicingMode | None = None - isSelfInvoicing: bool | None = None - ksefNumber: KsefNumber | None = None - sellerNip: Nip | None = None - + amount: Optional[InvoiceQueryAmount] = None + buyerIdentifier: Optional[InvoiceQueryBuyerIdentifier] = None + currencyCodes: Optional[list[CurrencyCode]] = None + formType: Optional[InvoiceQueryFormType] = None + hasAttachment: Optional[bool] = None + invoiceNumber: Optional[str] = None + invoiceTypes: Optional[list[InvoiceType]] = None + invoicingMode: Optional[InvoicingMode] = None + isSelfInvoicing: Optional[bool] = None + ksefNumber: Optional[KsefNumber] = None + sellerNip: Optional[Nip] = None @dataclass(frozen=True) class InvoiceStatusInfo(OpenApiModel): code: int description: str - details: list[str] | None = None - extensions: dict[str, str | None] | None = None - + details: Optional[list[str]] = None + extensions: Optional[dict[str, Optional[str]]] = None @dataclass(frozen=True) class OnlineSessionContextLimitsOverride(OpenApiModel): @@ -1449,110 +1275,94 @@ class OnlineSessionContextLimitsOverride(OpenApiModel): maxInvoiceWithAttachmentSizeInMB: int maxInvoices: int - @dataclass(frozen=True) class OnlineSessionEffectiveContextLimits(OpenApiModel): maxInvoiceSizeInMB: int maxInvoiceWithAttachmentSizeInMB: int maxInvoices: int - @dataclass(frozen=True) class OpenBatchSessionRequest(OpenApiModel): batchFile: BatchFileInfo encryption: EncryptionInfo formCode: FormCode - offlineMode: bool | None = None - + offlineMode: Optional[bool] = None @dataclass(frozen=True) class OpenBatchSessionResponse(OpenApiModel): partUploadRequests: list[PartUploadRequest] referenceNumber: ReferenceNumber - @dataclass(frozen=True) class OpenOnlineSessionRequest(OpenApiModel): encryption: EncryptionInfo formCode: FormCode - @dataclass(frozen=True) class OpenOnlineSessionResponse(OpenApiModel): referenceNumber: ReferenceNumber validUntil: str - @dataclass(frozen=True) class PartUploadRequest(OpenApiModel): - headers: dict[str, str | None] + headers: dict[str, Optional[str]] method: str ordinalNumber: int url: str - @dataclass(frozen=True) class PeppolProvider(OpenApiModel): dateCreated: str id: PeppolId name: str - @dataclass(frozen=True) class PermissionsEuEntityDetails(OpenApiModel): address: str fullName: str - @dataclass(frozen=True) class PermissionsOperationResponse(OpenApiModel): referenceNumber: ReferenceNumber - @dataclass(frozen=True) class PermissionsOperationStatusResponse(OpenApiModel): status: StatusInfo - @dataclass(frozen=True) class PermissionsSubjectEntityByFingerprintDetails(OpenApiModel): fullName: str subjectDetailsType: EntitySubjectByFingerprintDetailsType - address: str | None = None - + address: Optional[str] = None @dataclass(frozen=True) class PermissionsSubjectEntityByIdentifierDetails(OpenApiModel): fullName: str subjectDetailsType: EntitySubjectByIdentifierDetailsType - @dataclass(frozen=True) class PermissionsSubjectEntityDetails(OpenApiModel): fullName: str subjectDetailsType: EntitySubjectDetailsType - address: str | None = None - + address: Optional[str] = None @dataclass(frozen=True) class PermissionsSubjectPersonByFingerprintDetails(OpenApiModel): firstName: str lastName: str subjectDetailsType: PersonSubjectByFingerprintDetailsType - birthDate: str | None = None - idDocument: IdDocument | None = None - personIdentifier: PersonIdentifier | None = None - + birthDate: Optional[str] = None + idDocument: Optional[IdDocument] = None + personIdentifier: Optional[PersonIdentifier] = None @dataclass(frozen=True) class PermissionsSubjectPersonDetails(OpenApiModel): firstName: str lastName: str subjectDetailsType: PersonSubjectDetailsType - birthDate: str | None = None - idDocument: IdDocument | None = None - personIdentifier: PersonIdentifier | None = None - + birthDate: Optional[str] = None + idDocument: Optional[IdDocument] = None + personIdentifier: Optional[PersonIdentifier] = None @dataclass(frozen=True) class PersonByFingerprintWithIdentifierDetails(OpenApiModel): @@ -1560,7 +1370,6 @@ class PersonByFingerprintWithIdentifierDetails(OpenApiModel): identifier: PersonIdentifier lastName: str - @dataclass(frozen=True) class PersonByFingerprintWithoutIdentifierDetails(OpenApiModel): birthDate: str @@ -1568,29 +1377,25 @@ class PersonByFingerprintWithoutIdentifierDetails(OpenApiModel): idDocument: IdDocument lastName: str - @dataclass(frozen=True) class PersonCreateRequest(OpenApiModel): description: str isBailiff: bool nip: Nip pesel: Pesel - createdDate: str | None = None - isDeceased: bool | None = None - + createdDate: Optional[str] = None + isDeceased: Optional[bool] = None @dataclass(frozen=True) class PersonDetails(OpenApiModel): firstName: str lastName: str - @dataclass(frozen=True) class PersonIdentifier(OpenApiModel): type: PersonIdentifierType value: str - @dataclass(frozen=True) class PersonPermission(OpenApiModel): authorIdentifier: PersonPermissionsAuthorIdentifier @@ -1601,38 +1406,33 @@ class PersonPermission(OpenApiModel): permissionScope: PersonPermissionScope permissionState: PermissionState startDate: str - contextIdentifier: PersonPermissionsContextIdentifier | None = None - subjectEntityDetails: PermissionsSubjectEntityDetails | None = None - subjectPersonDetails: PermissionsSubjectPersonDetails | None = None - targetIdentifier: PersonPermissionsTargetIdentifier | None = None - + contextIdentifier: Optional[PersonPermissionsContextIdentifier] = None + subjectEntityDetails: Optional[PermissionsSubjectEntityDetails] = None + subjectPersonDetails: Optional[PermissionsSubjectPersonDetails] = None + targetIdentifier: Optional[PersonPermissionsTargetIdentifier] = None @dataclass(frozen=True) class PersonPermissionSubjectDetails(OpenApiModel): subjectDetailsType: PersonPermissionSubjectDetailsType - personByFpNoId: PersonByFingerprintWithoutIdentifierDetails | None = None - personByFpWithId: PersonByFingerprintWithIdentifierDetails | None = None - personById: PersonDetails | None = None - + personByFpNoId: Optional[PersonByFingerprintWithoutIdentifierDetails] = None + personByFpWithId: Optional[PersonByFingerprintWithIdentifierDetails] = None + personById: Optional[PersonDetails] = None @dataclass(frozen=True) class PersonPermissionsAuthorIdentifier(OpenApiModel): type: PersonPermissionsAuthorIdentifierType - value: str | None = None - + value: Optional[str] = None @dataclass(frozen=True) class PersonPermissionsAuthorizedIdentifier(OpenApiModel): type: PersonPermissionsAuthorizedIdentifierType value: str - @dataclass(frozen=True) class PersonPermissionsContextIdentifier(OpenApiModel): type: PersonPermissionsContextIdentifierType value: str - @dataclass(frozen=True) class PersonPermissionsGrantRequest(OpenApiModel): description: str @@ -1640,35 +1440,30 @@ class PersonPermissionsGrantRequest(OpenApiModel): subjectDetails: PersonPermissionSubjectDetails subjectIdentifier: PersonPermissionsSubjectIdentifier - @dataclass(frozen=True) class PersonPermissionsQueryRequest(OpenApiModel): queryType: PersonPermissionsQueryType - authorIdentifier: PersonPermissionsAuthorIdentifier | None = None - authorizedIdentifier: PersonPermissionsAuthorizedIdentifier | None = None - contextIdentifier: PersonPermissionsContextIdentifier | None = None - permissionState: PermissionState | None = None - permissionTypes: list[PersonPermissionType] | None = None - targetIdentifier: PersonPermissionsTargetIdentifier | None = None - + authorIdentifier: Optional[PersonPermissionsAuthorIdentifier] = None + authorizedIdentifier: Optional[PersonPermissionsAuthorizedIdentifier] = None + contextIdentifier: Optional[PersonPermissionsContextIdentifier] = None + permissionState: Optional[PermissionState] = None + permissionTypes: Optional[list[PersonPermissionType]] = None + targetIdentifier: Optional[PersonPermissionsTargetIdentifier] = None @dataclass(frozen=True) class PersonPermissionsSubjectIdentifier(OpenApiModel): type: PersonPermissionsSubjectIdentifierType value: str - @dataclass(frozen=True) class PersonPermissionsTargetIdentifier(OpenApiModel): type: PersonPermissionsTargetIdentifierType - value: str | None = None - + value: Optional[str] = None @dataclass(frozen=True) class PersonRemoveRequest(OpenApiModel): nip: Nip - @dataclass(frozen=True) class PersonalPermission(OpenApiModel): canDelegate: bool @@ -1677,38 +1472,33 @@ class PersonalPermission(OpenApiModel): permissionScope: PersonalPermissionScope permissionState: PermissionState startDate: str - authorizedIdentifier: PersonalPermissionsAuthorizedIdentifier | None = None - contextIdentifier: PersonalPermissionsContextIdentifier | None = None - subjectEntityDetails: PermissionsSubjectEntityDetails | None = None - subjectPersonDetails: PermissionsSubjectPersonDetails | None = None - targetIdentifier: PersonalPermissionsTargetIdentifier | None = None - + authorizedIdentifier: Optional[PersonalPermissionsAuthorizedIdentifier] = None + contextIdentifier: Optional[PersonalPermissionsContextIdentifier] = None + subjectEntityDetails: Optional[PermissionsSubjectEntityDetails] = None + subjectPersonDetails: Optional[PermissionsSubjectPersonDetails] = None + targetIdentifier: Optional[PersonalPermissionsTargetIdentifier] = None @dataclass(frozen=True) class PersonalPermissionsAuthorizedIdentifier(OpenApiModel): type: PersonalPermissionsAuthorizedIdentifierType value: str - @dataclass(frozen=True) class PersonalPermissionsContextIdentifier(OpenApiModel): type: PersonalPermissionsContextIdentifierType value: str - @dataclass(frozen=True) class PersonalPermissionsQueryRequest(OpenApiModel): - contextIdentifier: PersonalPermissionsContextIdentifier | None = None - permissionState: PermissionState | None = None - permissionTypes: list[PersonalPermissionType] | None = None - targetIdentifier: PersonalPermissionsTargetIdentifier | None = None - + contextIdentifier: Optional[PersonalPermissionsContextIdentifier] = None + permissionState: Optional[PermissionState] = None + permissionTypes: Optional[list[PersonalPermissionType]] = None + targetIdentifier: Optional[PersonalPermissionsTargetIdentifier] = None @dataclass(frozen=True) class PersonalPermissionsTargetIdentifier(OpenApiModel): type: PersonalPermissionsTargetIdentifierType - value: str | None = None - + value: Optional[str] = None @dataclass(frozen=True) class PublicKeyCertificate(OpenApiModel): @@ -1717,83 +1507,70 @@ class PublicKeyCertificate(OpenApiModel): validFrom: str validTo: str - @dataclass(frozen=True) class QueryCertificatesRequest(OpenApiModel): - certificateSerialNumber: str | None = None - expiresAfter: str | None = None - name: str | None = None - status: CertificateListItemStatus | None = None - type: KsefCertificateType | None = None - + certificateSerialNumber: Optional[str] = None + expiresAfter: Optional[str] = None + name: Optional[str] = None + status: Optional[CertificateListItemStatus] = None + type: Optional[KsefCertificateType] = None @dataclass(frozen=True) class QueryCertificatesResponse(OpenApiModel): certificates: list[CertificateListItem] hasMore: bool - @dataclass(frozen=True) class QueryEntityAuthorizationPermissionsResponse(OpenApiModel): authorizationGrants: list[EntityAuthorizationGrant] hasMore: bool - @dataclass(frozen=True) class QueryEntityRolesResponse(OpenApiModel): hasMore: bool roles: list[EntityRole] - @dataclass(frozen=True) class QueryEuEntityPermissionsResponse(OpenApiModel): hasMore: bool permissions: list[EuEntityPermission] - @dataclass(frozen=True) class QueryInvoicesMetadataResponse(OpenApiModel): hasMore: bool invoices: list[InvoiceMetadata] isTruncated: bool - permanentStorageHwmDate: str | None = None - + permanentStorageHwmDate: Optional[str] = None @dataclass(frozen=True) class QueryPeppolProvidersResponse(OpenApiModel): hasMore: bool peppolProviders: list[PeppolProvider] - @dataclass(frozen=True) class QueryPersonPermissionsResponse(OpenApiModel): hasMore: bool permissions: list[PersonPermission] - @dataclass(frozen=True) class QueryPersonalPermissionsResponse(OpenApiModel): hasMore: bool permissions: list[PersonalPermission] - @dataclass(frozen=True) class QuerySubordinateEntityRolesResponse(OpenApiModel): hasMore: bool roles: list[SubordinateEntityRole] - @dataclass(frozen=True) class QuerySubunitPermissionsResponse(OpenApiModel): hasMore: bool permissions: list[SubunitPermission] - @dataclass(frozen=True) class QueryTokensResponse(OpenApiModel): tokens: list[QueryTokensResponseItem] - continuationToken: str | None = None - + continuationToken: Optional[str] = None @dataclass(frozen=True) class QueryTokensResponseItem(OpenApiModel): @@ -1804,9 +1581,8 @@ class QueryTokensResponseItem(OpenApiModel): referenceNumber: ReferenceNumber requestedPermissions: list[TokenPermissionType] status: AuthenticationTokenStatus - lastUseDate: str | None = None - statusDetails: list[str] | None = None - + lastUseDate: Optional[str] = None + statusDetails: Optional[list[str]] = None @dataclass(frozen=True) class RetrieveCertificatesListItem(OpenApiModel): @@ -1815,21 +1591,17 @@ class RetrieveCertificatesListItem(OpenApiModel): certificateSerialNumber: str certificateType: KsefCertificateType - @dataclass(frozen=True) class RetrieveCertificatesRequest(OpenApiModel): certificateSerialNumbers: list[str] - @dataclass(frozen=True) class RetrieveCertificatesResponse(OpenApiModel): certificates: list[RetrieveCertificatesListItem] - @dataclass(frozen=True) class RevokeCertificateRequest(OpenApiModel): - revocationReason: CertificateRevocationReason | None = None - + revocationReason: Optional[CertificateRevocationReason] = None @dataclass(frozen=True) class SendInvoiceRequest(OpenApiModel): @@ -1838,15 +1610,13 @@ class SendInvoiceRequest(OpenApiModel): encryptedInvoiceSize: int invoiceHash: Sha256HashBase64 invoiceSize: int - hashOfCorrectedInvoice: Sha256HashBase64 | None = None - offlineMode: bool | None = None - + hashOfCorrectedInvoice: Optional[Sha256HashBase64] = None + offlineMode: Optional[bool] = None @dataclass(frozen=True) class SendInvoiceResponse(OpenApiModel): referenceNumber: ReferenceNumber - @dataclass(frozen=True) class SessionInvoiceStatusResponse(OpenApiModel): invoiceHash: Sha256HashBase64 @@ -1854,39 +1624,35 @@ class SessionInvoiceStatusResponse(OpenApiModel): ordinalNumber: int referenceNumber: ReferenceNumber status: InvoiceStatusInfo - acquisitionDate: str | None = None - invoiceFileName: str | None = None - invoiceNumber: str | None = None - invoicingMode: InvoicingMode | None = None - ksefNumber: KsefNumber | None = None - permanentStorageDate: str | None = None - upoDownloadUrl: str | None = None - upoDownloadUrlExpirationDate: str | None = None - + acquisitionDate: Optional[str] = None + invoiceFileName: Optional[str] = None + invoiceNumber: Optional[str] = None + invoicingMode: Optional[InvoicingMode] = None + ksefNumber: Optional[KsefNumber] = None + permanentStorageDate: Optional[str] = None + upoDownloadUrl: Optional[str] = None + upoDownloadUrlExpirationDate: Optional[str] = None @dataclass(frozen=True) class SessionInvoicesResponse(OpenApiModel): invoices: list[SessionInvoiceStatusResponse] - continuationToken: str | None = None - + continuationToken: Optional[str] = None @dataclass(frozen=True) class SessionStatusResponse(OpenApiModel): dateCreated: str dateUpdated: str status: StatusInfo - failedInvoiceCount: int | None = None - invoiceCount: int | None = None - successfulInvoiceCount: int | None = None - upo: UpoResponse | None = None - validUntil: str | None = None - + failedInvoiceCount: Optional[int] = None + invoiceCount: Optional[int] = None + successfulInvoiceCount: Optional[int] = None + upo: Optional[UpoResponse] = None + validUntil: Optional[str] = None @dataclass(frozen=True) class SessionsQueryResponse(OpenApiModel): sessions: list[SessionsQueryResponseItem] - continuationToken: str | None = None - + continuationToken: Optional[str] = None @dataclass(frozen=True) class SessionsQueryResponseItem(OpenApiModel): @@ -1897,48 +1663,41 @@ class SessionsQueryResponseItem(OpenApiModel): status: StatusInfo successfulInvoiceCount: int totalInvoiceCount: int - validUntil: str | None = None - + validUntil: Optional[str] = None @dataclass(frozen=True) class SetRateLimitsRequest(OpenApiModel): rateLimits: ApiRateLimitsOverride - @dataclass(frozen=True) class SetSessionLimitsRequest(OpenApiModel): batchSession: BatchSessionContextLimitsOverride onlineSession: OnlineSessionContextLimitsOverride - @dataclass(frozen=True) class SetSubjectLimitsRequest(OpenApiModel): - certificate: CertificateSubjectLimitsOverride | None = None - enrollment: EnrollmentSubjectLimitsOverride | None = None - subjectIdentifierType: SubjectIdentifierType | None = None - + certificate: Optional[CertificateSubjectLimitsOverride] = None + enrollment: Optional[EnrollmentSubjectLimitsOverride] = None + subjectIdentifierType: Optional[SubjectIdentifierType] = None @dataclass(frozen=True) class StatusInfo(OpenApiModel): code: int description: str - details: list[str] | None = None - + details: Optional[list[str]] = None @dataclass(frozen=True) class SubjectCreateRequest(OpenApiModel): description: str subjectNip: Nip subjectType: SubjectType - createdDate: str | None = None - subunits: list[Subunit] | None = None - + createdDate: Optional[str] = None + subunits: Optional[list[Subunit]] = None @dataclass(frozen=True) class SubjectRemoveRequest(OpenApiModel): subjectNip: Nip - @dataclass(frozen=True) class SubordinateEntityRole(OpenApiModel): description: str @@ -1946,24 +1705,20 @@ class SubordinateEntityRole(OpenApiModel): startDate: str subordinateEntityIdentifier: SubordinateRoleSubordinateEntityIdentifier - @dataclass(frozen=True) class SubordinateEntityRolesQueryRequest(OpenApiModel): - subordinateEntityIdentifier: EntityPermissionsSubordinateEntityIdentifier | None = None - + subordinateEntityIdentifier: Optional[EntityPermissionsSubordinateEntityIdentifier] = None @dataclass(frozen=True) class SubordinateRoleSubordinateEntityIdentifier(OpenApiModel): type: SubordinateRoleSubordinateEntityIdentifierType value: str - @dataclass(frozen=True) class Subunit(OpenApiModel): description: str subjectNip: Nip - @dataclass(frozen=True) class SubunitPermission(OpenApiModel): authorIdentifier: SubunitPermissionsAuthorIdentifier @@ -1973,103 +1728,87 @@ class SubunitPermission(OpenApiModel): permissionScope: SubunitPermissionScope startDate: str subunitIdentifier: SubunitPermissionsSubunitIdentifier - subjectPersonDetails: PermissionsSubjectPersonDetails | None = None - subunitName: str | None = None - + subjectPersonDetails: Optional[PermissionsSubjectPersonDetails] = None + subunitName: Optional[str] = None @dataclass(frozen=True) class SubunitPermissionsAuthorIdentifier(OpenApiModel): type: SubunitPermissionsAuthorIdentifierType value: str - @dataclass(frozen=True) class SubunitPermissionsAuthorizedIdentifier(OpenApiModel): type: SubunitPermissionsSubjectIdentifierType value: str - @dataclass(frozen=True) class SubunitPermissionsContextIdentifier(OpenApiModel): type: SubunitPermissionsContextIdentifierType value: str - @dataclass(frozen=True) class SubunitPermissionsGrantRequest(OpenApiModel): contextIdentifier: SubunitPermissionsContextIdentifier description: str subjectDetails: PersonPermissionSubjectDetails subjectIdentifier: SubunitPermissionsSubjectIdentifier - subunitName: str | None = None - + subunitName: Optional[str] = None @dataclass(frozen=True) class SubunitPermissionsQueryRequest(OpenApiModel): - subunitIdentifier: SubunitPermissionsSubunitIdentifier | None = None - + subunitIdentifier: Optional[SubunitPermissionsSubunitIdentifier] = None @dataclass(frozen=True) class SubunitPermissionsSubjectIdentifier(OpenApiModel): type: SubunitPermissionsSubjectIdentifierType value: str - @dataclass(frozen=True) class SubunitPermissionsSubunitIdentifier(OpenApiModel): type: SubunitPermissionsSubunitIdentifierType value: str - @dataclass(frozen=True) class TestDataAuthorizedIdentifier(OpenApiModel): type: TestDataAuthorizedIdentifierType value: str - @dataclass(frozen=True) class TestDataContextIdentifier(OpenApiModel): type: TestDataContextIdentifierType value: str - @dataclass(frozen=True) class TestDataPermission(OpenApiModel): description: str permissionType: TestDataPermissionType - @dataclass(frozen=True) class TestDataPermissionsGrantRequest(OpenApiModel): authorizedIdentifier: TestDataAuthorizedIdentifier contextIdentifier: TestDataContextIdentifier permissions: list[TestDataPermission] - @dataclass(frozen=True) class TestDataPermissionsRevokeRequest(OpenApiModel): authorizedIdentifier: TestDataAuthorizedIdentifier contextIdentifier: TestDataContextIdentifier - @dataclass(frozen=True) class TokenAuthorIdentifierTypeIdentifier(OpenApiModel): type: TokenAuthorIdentifierType value: str - @dataclass(frozen=True) class TokenContextIdentifierTypeIdentifier(OpenApiModel): type: TokenContextIdentifierType value: str - @dataclass(frozen=True) class TokenInfo(OpenApiModel): token: str validUntil: str - @dataclass(frozen=True) class TokenStatusResponse(OpenApiModel): authorIdentifier: TokenAuthorIdentifierTypeIdentifier @@ -2079,22 +1818,19 @@ class TokenStatusResponse(OpenApiModel): referenceNumber: ReferenceNumber requestedPermissions: list[TokenPermissionType] status: AuthenticationTokenStatus - lastUseDate: str | None = None - statusDetails: list[str] | None = None - + lastUseDate: Optional[str] = None + statusDetails: Optional[list[str]] = None @dataclass(frozen=True) class TooManyRequestsResponse(OpenApiModel): status: dict[str, Any] - @dataclass(frozen=True) class UpoPageResponse(OpenApiModel): downloadUrl: str downloadUrlExpirationDate: str referenceNumber: ReferenceNumber - @dataclass(frozen=True) class UpoResponse(OpenApiModel): pages: list[UpoPageResponse] diff --git a/tests/test_clients.py b/tests/test_clients.py index 1ca6cc3..e340bb0 100644 --- a/tests/test_clients.py +++ b/tests/test_clients.py @@ -198,12 +198,8 @@ def test_limits_clients(self): rate_client.get_rate_limits("token") security_client = SecurityClient(self.http) - with ( - patch.object(security_client, "_request_json", Mock(return_value={"ok": True})), - patch.object(security_client, "_request_bytes", Mock(return_value=b"pem")), - ): + with patch.object(security_client, "_request_json", Mock(return_value={"ok": True})): security_client.get_public_key_certificates() - self.assertEqual(security_client.get_public_key_pem(), "pem") def test_testdata_client(self): client = TestDataClient(self.http) @@ -435,12 +431,8 @@ async def test_async_clients(self): await rate_limits.get_rate_limits("token") security = AsyncSecurityClient(http) - with ( - patch.object(security, "_request_json", AsyncMock(return_value={"ok": True})), - patch.object(security, "_request_bytes", AsyncMock(return_value=b"pem")), - ): + with patch.object(security, "_request_json", AsyncMock(return_value={"ok": True})): await security.get_public_key_certificates() - self.assertEqual(await security.get_public_key_pem(), "pem") testdata = AsyncTestDataClient(http) with patch.object(testdata, "_request_json", AsyncMock(return_value={"ok": True})): diff --git a/tools/check_coverage.py b/tools/check_coverage.py new file mode 100644 index 0000000..780404c --- /dev/null +++ b/tools/check_coverage.py @@ -0,0 +1,351 @@ +# ruff: noqa: E501 +import argparse +import ast +import json +import re +from dataclasses import dataclass +from pathlib import Path + + +def normalize_path(path: str) -> str: + # Replace {param} with {} to compare structure + return re.sub(r"{[^}]+}", "{}", path) + + +def extract_path_params(path: str) -> set[str]: + # Extract parameter names from OpenAPI path: /auth/{referenceNumber} -> {referenceNumber} + return set(re.findall(r"{([^}]+)}", path)) + + +@dataclass +class EndpointSpec: + method: str + path: str + normalized_path: str + path_params: set[str] + query_params: set[str] + deprecated: bool + success_codes: set[int] + + +@dataclass +class ImplementedEndpoint: + method: str + path_pattern: str + normalized_path: str + # Parameters detected in the f-string path construction + detected_path_vars: set[str] + # Parameters passed to params={} argument + detected_query_vars: set[str] + expected_status: set[int] + file_path: str + line_no: int + + +def get_openapi_specs(openapi_path: Path) -> dict[tuple[str, str], EndpointSpec]: + """Returns a map of (method, normalized_path) -> EndpointSpec from OpenAPI.""" + with open(openapi_path, encoding="utf-8") as f: + data = json.load(f) + + specs = {} + for path, methods in data.get("paths", {}).items(): + norm_path = normalize_path(path) + path_params = extract_path_params(path) + + for method_name, details in methods.items(): + method = method_name.upper() + + # Extract query parameters defined for this operation + query_params = set() + parameters = details.get("parameters", []) + for param in parameters: + if param.get("in") == "query": + query_params.add(param["name"]) + + deprecated = details.get("deprecated", False) + success_codes = set() + for code_str in details.get("responses", {}): + if code_str.startswith("2") and code_str.isdigit(): + success_codes.add(int(code_str)) + + spec = EndpointSpec( + method=method, + path=path, + normalized_path=norm_path, + path_params=path_params, + query_params=query_params, + deprecated=deprecated, + success_codes=success_codes, + ) + specs[(method, norm_path)] = spec + return specs + + +class AdvancedClientVisitor(ast.NodeVisitor): + def __init__(self, filename: str): + self.filename = filename + self.found_endpoints: list[ImplementedEndpoint] = [] + + def visit_Call(self, node): + # Look for self._request_json / self._request_bytes / self._request_raw + if isinstance(node.func, ast.Attribute) and node.func.attr in ( + "_request_json", + "_request_bytes", + "_request_no_auth", + "_request_raw", + ): + self._analyze_request_call(node) + self.generic_visit(node) + + def _analyze_request_call(self, node: ast.Call): + if len(node.args) < 2: + return + + method_arg = node.args[0] + path_arg = node.args[1] + + # Extract Method + method = self._extract_string_value(method_arg) + if not method: + return # Could not resolve method statically + + # Extract Path and Path Params (from f-strings) + path_pattern, detected_path_vars = self._extract_path_info(path_arg) + if not path_pattern: + return + + normalized_path = normalize_path(path_pattern) + + # Extract Query Params + detected_query_vars = self._extract_query_params(node) + expected_status = self._extract_expected_status(node) + + self.found_endpoints.append( + ImplementedEndpoint( + method=method.upper(), + path_pattern=path_pattern, + normalized_path=normalized_path, + detected_path_vars=detected_path_vars, + detected_query_vars=detected_query_vars, + expected_status=expected_status, + file_path=self.filename, + line_no=node.lineno, + ) + ) + + def _extract_string_value(self, node) -> str | None: + if isinstance(node, ast.Constant) and isinstance(node.value, str): + return node.value + return None + + def _extract_path_info(self, node) -> tuple[str | None, set[str]]: + if isinstance(node, ast.Constant) and isinstance(node.value, str): + return node.value, set() + + if isinstance(node, ast.JoinedStr): + # Convert f-string f"/auth/{ref}" to pattern "/auth/{}" and extract vars + pattern_parts = [] + vars_found = set() + + for part in node.values: + if isinstance(part, ast.Constant) and isinstance(part.value, str): + pattern_parts.append(part.value) + elif isinstance(part, ast.FormattedValue): + pattern_parts.append("{}") + # Try to get the variable name used in f-string + if isinstance(part.value, ast.Name): + vars_found.add(part.value.id) + + return "".join(pattern_parts), vars_found + + return None, set() + + def _extract_query_params(self, node: ast.Call) -> set[str]: + # Look for 'params' keyword argument + params_keywords = [kw for kw in node.keywords if kw.arg == "params"] + if not params_keywords: + return set() + + params_value = params_keywords[0].value + + # We handle: params={'page': page, 'limit': 10} + # or params=params (if params is a dict built earlier, hard to track statically perfectly) + + found_keys = set() + if isinstance(params_value, ast.Dict): + for key in params_value.keys: + if isinstance(key, ast.Constant) and isinstance(key.value, str): + found_keys.add(key.value) + + # If params passed as variable (e.g. params=page_params), we assume best effort or specific variable name tracking + # For now, we only statically analyze inline dict definitions or assume manual review if dynamic + return found_keys + + def _extract_expected_status(self, node: ast.Call) -> set[int]: + # Default is {200} if not specified + default_status = {200} + + status_keywords = [kw for kw in node.keywords if kw.arg == "expected_status"] + if not status_keywords: + return default_status + + val = status_keywords[0].value + + # Parse set: {200, 201} + if isinstance(val, ast.Set): + result = set() + for el in val.elts: + if isinstance(el, ast.Constant) and isinstance(el.value, int): + result.add(el.value) + return result + + # Parse list/tuple: [200, 202] + if isinstance(val, (ast.List, ast.Tuple)): + result = set() + for el in val.elts: + if isinstance(el, ast.Constant) and isinstance(el.value, int): + result.add(el.value) + return result + + # Parse constant: 200 (if passed as single int, though type hint says set) + if isinstance(val, ast.Constant) and isinstance(val.value, int): + return {val.value} + + # If passed as None, treat as default check (usually 200) + if isinstance(val, ast.Constant) and val.value is None: + return default_status + + # If dynamic variable, we can't infer easily -> return empty set or default? + # Let's return default to avoid noise, or empty to skip check. + # Returning empty set effectively disables the check for this call. + return set() + + +def get_implemented_endpoints_deep(source_dir: Path) -> list[ImplementedEndpoint]: + endpoints = [] + for py_file in source_dir.rglob("*.py"): + try: + visitor = AdvancedClientVisitor(str(py_file)) + tree = ast.parse(py_file.read_text(encoding="utf-8")) + visitor.visit(tree) + endpoints.extend(visitor.found_endpoints) + except Exception as e: + print(f"Error parsing {py_file}: {e}") + return endpoints + + +def to_camel_case(snake_str: str) -> str: + # reference_number -> referenceNumber + components = snake_str.split("_") + return components[0] + "".join(x.title() for x in components[1:]) + + +def main(): + # Re-declare dataclasses here for standalone script execution context if needed + # (though already defined above, ensuring valid scope) + + parser = argparse.ArgumentParser(description="Deep API coverage check.") + parser.add_argument("--openapi", required=True, type=Path, help="Path to open-api.json") + parser.add_argument( + "--src", required=True, type=Path, help="Path to source directory containing clients" + ) + args = parser.parse_args() + + # 1. Load OpenAPI Specs + openapi_specs = get_openapi_specs(args.openapi) + + # 2. Analyze Code + implemented_eps = get_implemented_endpoints_deep(args.src) + + # 3. Compare + openapi_keys = set(openapi_specs.keys()) + implemented_keys = set((ep.method, ep.normalized_path) for ep in implemented_eps) + + missing = openapi_keys - implemented_keys + extra = implemented_keys - openapi_keys + + print(f"OpenAPI Endpoints: {len(openapi_keys)}") + print(f"Implemented Endpoints: {len(implemented_keys)}") + + issues_found = False + + if missing: + print("\n[MISSING] The following endpoints are logically missing in code:") + for method, path in sorted(missing): + print(f" - [{method}] {openapi_specs[(method, path)].path}") + issues_found = True + + if extra: + print("\n[EXTRA] The following endpoints found in code but NOT in OpenAPI:") + for method, path in sorted(extra): + # Try to find file info + matches = [ + x for x in implemented_eps if x.method == method and x.normalized_path == path + ] + for m in matches: + print(f" - [{method}] {m.path_pattern} (at {m.file_path}:{m.line_no})") + # Extra endpoints might be issues if they are typos + if extra: + print(" (Note: This might be due to typos in URL or unofficial endpoints)") + issues_found = True + + # Deep Analysis: Params + print("\n[DEEP ANALYSIS] Checking Path & Query Parameters...") + + # Map implementation to spec for verification + for impl in implemented_eps: + spec = openapi_specs.get((impl.method, impl.normalized_path)) + if not spec: + continue # Extra endpoint implemented? or ignored + + # Check Path Params + # OpenAPI: {referenceNumber} -> Python: reference_number (snake case conversion usually) + # We try to match count. + if len(impl.detected_path_vars) != len(spec.path_params): + # Heuristic check + print(f" [WARN] Path param mismatch for {impl.method} {spec.path}") + print(f" Expected: {spec.path_params}") + print( + f" Found in f-string: {impl.detected_path_vars} at {impl.file_path}:{impl.line_no}" + ) + + # Check Query Params + # We only check if the KEY names used in params={} dictionary exist in OpenAPI definition + # OpenAPI params are usually camelCase (pageSize). Python code should use pageSize key in dict. + for query_key in impl.detected_query_vars: + if query_key not in spec.query_params: + # Common false positive: 'page' vs 'Page', or continuationToken + print( + f" [WARN] Unknown query param '{query_key}' used in code for {impl.method} {spec.path}" + ) + print(f" Allowed: {spec.query_params} at {impl.file_path}:{impl.line_no}") + + # Check Deprecated + if spec.deprecated: + print(f" [WARN] Endpoint is DEPRECATED: {impl.method} {spec.path} at {impl.file_path}:{impl.line_no}") + + # Check Status Codes + # We warn if OpenAPI declares success codes that are NOT covered by expect_status + # Example: API returns 201, code expects {200} -> Mismatch + if impl.expected_status and spec.success_codes: + # We want expected_status to match success_codes exactly? Or be a superset? + # Usually: expected_status should contain ALL success_codes returned by API. + # If API returns 201 and we only handle 200, client might throw error. + missing_codes = spec.success_codes - impl.expected_status + if missing_codes: + print(f" [WARN] Status code mismatch for {impl.method} {spec.path}") + print(f" OpenAPI returns: {spec.success_codes}") + print(f" Code expects: {impl.expected_status}") + + if issues_found: + print("\nCoverage check FAILED.") + exit(1) + else: + print("\nCoverage check PASSED (Structure Match). Review Warnings above.") + exit(0) + + +if __name__ == "__main__": + from dataclasses import dataclass + + main() diff --git a/tools/generate_openapi_models.py b/tools/generate_openapi_models.py index abc6590..206edc0 100644 --- a/tools/generate_openapi_models.py +++ b/tools/generate_openapi_models.py @@ -156,6 +156,7 @@ def generate_models(input_path: Path, output_path: Path) -> None: alias_names.append(name) lines: list[str] = [ + "# ruff: noqa", "# Generated from ksef-docs/open-api.json. Do not edit manually.", "from __future__ import annotations", "", @@ -221,7 +222,7 @@ def generate_models(input_path: Path, output_path: Path) -> None: ' raise ValueError("data is None")', " type_map = _get_type_map(cls)", " kwargs: dict[str, Any] = {}", - " for model_field in fields(cls):", + " for model_field in fields(cls): # type: ignore", ' json_key = model_field.metadata.get("json_key", model_field.name)', " if json_key in data:", " type_hint = type_map.get(model_field.name, Any)", @@ -230,7 +231,7 @@ def generate_models(input_path: Path, output_path: Path) -> None: "", " def to_dict(self, omit_none: bool = True) -> dict[str, Any]:", " result: dict[str, Any] = {}", - " for model_field in fields(self):", + " for model_field in fields(self): # type: ignore", ' json_key = model_field.metadata.get("json_key", model_field.name)', " value = getattr(self, model_field.name)", " if omit_none and value is None:", @@ -252,7 +253,8 @@ def generate_models(input_path: Path, output_path: Path) -> None: lines.extend(_generate_object(name, schemas[name])) lines.append("") - output_path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") + with output_path.open("w", encoding="utf-8", newline="\n") as f: + f.write("\n".join(lines).rstrip() + "\n") def main() -> None: