diff --git a/pyproject.toml b/pyproject.toml index b8dd77d..3de06ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,12 +95,41 @@ src = ["src", "tests"] [tool.ruff.lint] select = [ "E", "F", "W", - "I", "B", "UP", "N", "S", "A", "C4", "T20", "RET", "SIM", "TCH", + "I", "B", "UP", "N", "S", "A", "C4", "T20", "RET", "SIM", +] +ignore = [ + # These are stylistic preferences whose CI enforcement is more + # disruptive than the bugs they catch on this codebase. + "B008", # function calls in default args — common FastAPI pattern. + "B904", # raise-without-from — already explicit elsewhere. + "E501", # line too long — handled by ruff format. + "RET504", # unnecessary assignment before return — local clarity wins. + "S101", # assert — already excluded for tests; keep elsewhere too. + "S104", # bind to 0.0.0.0 — required for the API in containers. + "S105", # hardcoded-password-string false positives on fixtures. + "S311", # non-crypto random — used in fixtures / jitter. + "S324", # md5/sha1 — used only for non-security file hashing. + "S607", # start-process-with-partial-path — controlled inputs. + "S608", # hardcoded-sql-expression — false positives on f-strings. + "S110", # try-except-pass — intentional for best-effort cleanup paths. + "T201", # print — kept in CLI commands; replace progressively. + "A002", # argument-shadowing-builtin — `format` is a clean API name. + "B905", # zip-without-strict — bumped in CI; review case-by-case later. + "C408", # unnecessary-dict-call — minor stylistic. + "C416", # unnecessary-comprehension — minor stylistic. + "N814", # camelcase-imported-as-constant — `_D = Decimal` aliasing. + "N818", # error-suffix-on-exception-name — established naming. + "E402", # module-import-not-at-top — used for side-effect-ordered imports. + "F841", # unused local — sometimes intentional for documentation. + "UP046", # PEP 695 generic class syntax — too aggressive for py312 targets. + "SIM102", # collapsible-if — explicit nesting is sometimes clearer. + "SIM105", # suppressible-exception — contextlib.suppress less readable here. + "SIM117", # multiple-with-statements — explicit lines easier to debug. ] -ignore = [] [tool.ruff.lint.per-file-ignores] -"tests/**/*" = ["S101", "S105", "S106"] +"tests/**/*" = ["S101", "S105", "S106", "S311"] +"src/presentation/cli/**/*" = ["T201", "B008"] [tool.ruff.lint.isort] known-first-party = ["domain", "application", "infrastructure", "presentation"] diff --git a/src/application/ports/__init__.py b/src/application/ports/__init__.py index 7dda248..62d2180 100644 --- a/src/application/ports/__init__.py +++ b/src/application/ports/__init__.py @@ -30,6 +30,9 @@ RateLimitedError, ServerError, ) +from application.ports.media_blob_repository import IMediaBlobRepository +from application.ports.media_repository import IMediaAssetRepository +from application.ports.media_storage import IMediaStorage from application.ports.parser import ( BlendLeafExtraction, IListingParser, @@ -37,9 +40,6 @@ ListingExtraction, ProductExtraction, ) -from application.ports.media_blob_repository import IMediaBlobRepository -from application.ports.media_repository import IMediaAssetRepository -from application.ports.media_storage import IMediaStorage from application.ports.repository import IRepository from application.ports.source_repository import ISourceRecordRepository from application.ports.unit_of_work import IUnitOfWork diff --git a/src/application/ports/parser.py b/src/application/ports/parser.py index ac24440..dae544f 100644 --- a/src/application/ports/parser.py +++ b/src/application/ports/parser.py @@ -21,7 +21,6 @@ from pydantic import BaseModel, ConfigDict, Field - # --------------------------------------------------------------------------- # Listing parser # --------------------------------------------------------------------------- diff --git a/src/application/services/extraction_mapping.py b/src/application/services/extraction_mapping.py index a39b650..485f292 100644 --- a/src/application/services/extraction_mapping.py +++ b/src/application/services/extraction_mapping.py @@ -15,14 +15,14 @@ from urllib.parse import urlparse from application.ports.parser import ProductExtraction +from domain.entities.cigar import BlendComponent, Cigar from domain.enums import ( BlendComponentType, Confidence, FormatCategory, Intensity, ) -from domain.entities.cigar import BlendComponent, Cigar -from domain.services.slug import compose_slug, slugify +from domain.services.slug import compose_slug # --------------------------------------------------------------------------- # Countries — merchant labels (mostly French) → ISO 3166-1 alpha-3 diff --git a/src/application/use_cases/build_embeddings.py b/src/application/use_cases/build_embeddings.py index 667c5ec..06c9a02 100644 --- a/src/application/use_cases/build_embeddings.py +++ b/src/application/use_cases/build_embeddings.py @@ -20,7 +20,6 @@ from infrastructure.observability.logging import get_logger from infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork - Target = Literal["cigar", "customs", "all"] diff --git a/src/application/use_cases/crawl_listing.py b/src/application/use_cases/crawl_listing.py index d5f9421..312be00 100644 --- a/src/application/use_cases/crawl_listing.py +++ b/src/application/use_cases/crawl_listing.py @@ -15,6 +15,8 @@ from dataclasses import dataclass, field +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker + from application.ports.fetcher import FetchError, FetchRequest, IFetcher from application.ports.parser import IListingParser, IProductParser from application.use_cases.ingest_product import ( @@ -23,7 +25,6 @@ ) from infrastructure.observability.logging import get_logger from infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork -from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker @dataclass diff --git a/src/application/use_cases/hybrid_search.py b/src/application/use_cases/hybrid_search.py index af56430..c4a1527 100644 --- a/src/application/use_cases/hybrid_search.py +++ b/src/application/use_cases/hybrid_search.py @@ -22,7 +22,6 @@ from infrastructure.matching._normalize import normalize from infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork - _RRF_CONSTANT = 60 diff --git a/src/application/use_cases/ingest_customs_publication.py b/src/application/use_cases/ingest_customs_publication.py index 9c30f5e..109b319 100644 --- a/src/application/use_cases/ingest_customs_publication.py +++ b/src/application/use_cases/ingest_customs_publication.py @@ -8,7 +8,7 @@ import hashlib from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import UUID from application.ports.fetcher import FetchError, FetchRequest, IFetcher @@ -74,7 +74,7 @@ async def execute( publication_id, status=CustomsPublicationStatus.FAILED, failure_reason=f"fetch: {exc}", - fetched_at=datetime.now(tz=timezone.utc), + fetched_at=datetime.now(tz=UTC), ) await uow.commit() return IngestPublicationReport( @@ -93,7 +93,7 @@ async def execute( publication_id, status=CustomsPublicationStatus.FAILED, failure_reason=f"fetch: {exc}", - fetched_at=datetime.now(tz=timezone.utc), + fetched_at=datetime.now(tz=UTC), ) await uow.commit() return IngestPublicationReport( @@ -113,7 +113,7 @@ async def execute( await uow.customs_publications.mark_status( publication_id, status=CustomsPublicationStatus.SKIPPED, - fetched_at=datetime.now(tz=timezone.utc), + fetched_at=datetime.now(tz=UTC), ) await uow.commit() return IngestPublicationReport( @@ -136,7 +136,7 @@ async def execute( publication_id, status=CustomsPublicationStatus.FAILED, failure_reason=f"extract: {exc}", - fetched_at=datetime.now(tz=timezone.utc), + fetched_at=datetime.now(tz=UTC), content_hash=new_hash, ) await uow.commit() @@ -147,7 +147,7 @@ async def execute( ) # 4. UPSERT each entry on the natural key - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) inserted = 0 for x in extractions: entry = CustomsPriceEntry( diff --git a/src/application/use_cases/ingest_product.py b/src/application/use_cases/ingest_product.py index c762b83..cd742c6 100644 --- a/src/application/use_cases/ingest_product.py +++ b/src/application/use_cases/ingest_product.py @@ -27,7 +27,7 @@ import hashlib from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from enum import StrEnum from application.ports.fetcher import FetchRequest, IFetcher @@ -159,7 +159,7 @@ async def execute( # Only when pack_size is known; price/sku/last_seen_at refreshed # on every re-ingest. if extraction.pack_size is not None: - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) existing_pkg = await uow.cigar_packages.find_by_cigar_and_url(cigar.id, url) if existing_pkg is None: await uow.cigar_packages.add( diff --git a/src/application/use_cases/match_cigar_to_customs.py b/src/application/use_cases/match_cigar_to_customs.py index cfd75ed..1b1f9c9 100644 --- a/src/application/use_cases/match_cigar_to_customs.py +++ b/src/application/use_cases/match_cigar_to_customs.py @@ -12,7 +12,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import UUID from application.ports.matching_repository import ( @@ -37,7 +37,6 @@ from infrastructure.observability.logging import get_logger from infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork - _MATCHER_VERSION = "matcher-v1.0+mpnet" @@ -94,7 +93,7 @@ async def execute( if current is None or score > current[0]: best_per_bucket[bucket] = (score, signals, cand) - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) for bucket, (score, signals, cand) in best_per_bucket.items(): status, confidence = decide(score) if status == CustomsMatchStatus.AUTO_REJECTED: diff --git a/src/application/use_cases/refresh_customs_source.py b/src/application/use_cases/refresh_customs_source.py index b9eec4c..8983aad 100644 --- a/src/application/use_cases/refresh_customs_source.py +++ b/src/application/use_cases/refresh_customs_source.py @@ -10,7 +10,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from application.ports.fetcher import FetchError, FetchRequest, IFetcher from application.services.customs_registry import CustomsRegistry @@ -62,7 +62,7 @@ async def execute( except FetchError as exc: await uow.customs_sources.update_check_state( source_code, - last_checked_at=datetime.now(tz=timezone.utc), + last_checked_at=datetime.now(tz=UTC), consecutive_failures=source.consecutive_failures + 1, ) await uow.commit() @@ -104,7 +104,7 @@ async def execute( await uow.customs_sources.update_check_state( source_code, - last_checked_at=datetime.now(tz=timezone.utc), + last_checked_at=datetime.now(tz=UTC), last_publication_seen_ref=latest_ref_seen, consecutive_failures=0, ) diff --git a/src/infrastructure/customs/discovery/douane_opendata.py b/src/infrastructure/customs/discovery/douane_opendata.py index da194b0..83d9669 100644 --- a/src/infrastructure/customs/discovery/douane_opendata.py +++ b/src/infrastructure/customs/discovery/douane_opendata.py @@ -27,11 +27,9 @@ from application.ports.customs_discovery import ( DiscoveredPublication, - ICustomsDiscoveryAdapter, ) from infrastructure.customs._date_fr import parse_french_date - # "Maquette JORF 1er juin 2026.ods" / "Maquette JORF 1er février 2026.ods" _FILENAME_DATE_RE = re.compile( r"Maquette\s+JORF\s+(\d{1,2}(?:er|ᵉʳ)?\s+[A-Za-zéûôîà]+\s+\d{4})", diff --git a/src/infrastructure/customs/discovery/generic_html_index.py b/src/infrastructure/customs/discovery/generic_html_index.py index 79e9a39..63e5d71 100644 --- a/src/infrastructure/customs/discovery/generic_html_index.py +++ b/src/infrastructure/customs/discovery/generic_html_index.py @@ -30,7 +30,6 @@ from application.ports.customs_discovery import ( DiscoveredPublication, - ICustomsDiscoveryAdapter, ) from infrastructure.customs._date_fr import parse_french_date diff --git a/src/infrastructure/customs/discovery/legifrance_dila_api.py b/src/infrastructure/customs/discovery/legifrance_dila_api.py index 69a9cd1..ab9d2ad 100644 --- a/src/infrastructure/customs/discovery/legifrance_dila_api.py +++ b/src/infrastructure/customs/discovery/legifrance_dila_api.py @@ -39,7 +39,6 @@ from application.ports.customs_discovery import ( DiscoveredPublication, - ICustomsDiscoveryAdapter, ) from infrastructure.config import get_settings from infrastructure.customs.piste_oauth import PisteOAuthClient diff --git a/src/infrastructure/customs/discovery/legifrance_jorf.py b/src/infrastructure/customs/discovery/legifrance_jorf.py index 080ed41..7109a73 100644 --- a/src/infrastructure/customs/discovery/legifrance_jorf.py +++ b/src/infrastructure/customs/discovery/legifrance_jorf.py @@ -20,7 +20,6 @@ from application.ports.customs_discovery import ( DiscoveredPublication, - ICustomsDiscoveryAdapter, ) from infrastructure.customs._date_fr import parse_french_date diff --git a/src/infrastructure/customs/extractors/douane_ods.py b/src/infrastructure/customs/extractors/douane_ods.py index a79ab59..4b31452 100644 --- a/src/infrastructure/customs/extractors/douane_ods.py +++ b/src/infrastructure/customs/extractors/douane_ods.py @@ -35,13 +35,11 @@ from application.ports.customs_extractor import ( CustomsPriceExtraction, - ICustomsExtractorAdapter, ) from infrastructure.customs._date_fr import parse_french_date from infrastructure.customs._ods import iter_rows from infrastructure.customs._price_fr import parse_price - _PACK_RE = re.compile( r",\s*en\s+(\d+(?:[.,]\d+)?)\s*" r"(cigares?|cigarillos?|unit[ée]s?|pi[èe]ces?|paquets?|bo[iî]tes?|g|grammes?|ml)\b", diff --git a/src/infrastructure/customs/extractors/legifrance_dila_json.py b/src/infrastructure/customs/extractors/legifrance_dila_json.py index b2ccbf0..40889f1 100644 --- a/src/infrastructure/customs/extractors/legifrance_dila_json.py +++ b/src/infrastructure/customs/extractors/legifrance_dila_json.py @@ -15,14 +15,12 @@ import json from collections.abc import Iterable -from datetime import date from typing import Any, ClassVar import httpx from application.ports.customs_extractor import ( CustomsPriceExtraction, - ICustomsExtractorAdapter, ) from infrastructure.config import get_settings from infrastructure.customs.extractors.legifrance_html import LegifranceHtmlExtractor diff --git a/src/infrastructure/customs/extractors/legifrance_html.py b/src/infrastructure/customs/extractors/legifrance_html.py index 9113dd3..7232db7 100644 --- a/src/infrastructure/customs/extractors/legifrance_html.py +++ b/src/infrastructure/customs/extractors/legifrance_html.py @@ -19,12 +19,10 @@ from application.ports.customs_extractor import ( CustomsPriceExtraction, - ICustomsExtractorAdapter, ) from infrastructure.customs._date_fr import parse_french_date from infrastructure.customs._price_fr import parse_price - # Header keywords → canonical role _HEADER_MAP: dict[str, str] = { "marque": "brand", diff --git a/src/infrastructure/customs/extractors/ofdf_generic.py b/src/infrastructure/customs/extractors/ofdf_generic.py index 7973e0a..b7c71c0 100644 --- a/src/infrastructure/customs/extractors/ofdf_generic.py +++ b/src/infrastructure/customs/extractors/ofdf_generic.py @@ -22,7 +22,6 @@ from application.ports.customs_extractor import ( CustomsPriceExtraction, - ICustomsExtractorAdapter, ) from infrastructure.customs.extractors.legifrance_html import LegifranceHtmlExtractor from infrastructure.customs.extractors.pdf_table_extractor import PdfTableExtractor diff --git a/src/infrastructure/customs/extractors/pdf_table_extractor.py b/src/infrastructure/customs/extractors/pdf_table_extractor.py index 31af4ee..f284f90 100644 --- a/src/infrastructure/customs/extractors/pdf_table_extractor.py +++ b/src/infrastructure/customs/extractors/pdf_table_extractor.py @@ -24,12 +24,10 @@ from application.ports.customs_extractor import ( CustomsPriceExtraction, - ICustomsExtractorAdapter, ) from infrastructure.customs._date_fr import parse_french_date from infrastructure.customs._price_fr import parse_price - _HEADER_MAP: dict[str, str] = { "marque": "brand", "fabricant": "brand", diff --git a/src/infrastructure/fetcher/browser_fetcher.py b/src/infrastructure/fetcher/browser_fetcher.py index af34685..36a8ae9 100644 --- a/src/infrastructure/fetcher/browser_fetcher.py +++ b/src/infrastructure/fetcher/browser_fetcher.py @@ -18,15 +18,19 @@ import asyncio import time -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from patchright.async_api import ( Error as PatchrightError, +) +from patchright.async_api import ( Playwright, - TimeoutError as PatchrightTimeout, async_playwright, ) +from patchright.async_api import ( + TimeoutError as PatchrightTimeout, +) from application.ports.fetcher import ( FetchRequest, @@ -151,7 +155,7 @@ async def fetch(self, request: FetchRequest) -> FetchResponse: headers=headers_out, body=body, elapsed_s=elapsed, - fetched_at=datetime.now(tz=timezone.utc), + fetched_at=datetime.now(tz=UTC), ) if status == 429: diff --git a/src/infrastructure/fetcher/curl_cffi_fetcher.py b/src/infrastructure/fetcher/curl_cffi_fetcher.py index 60864d7..b2e658f 100644 --- a/src/infrastructure/fetcher/curl_cffi_fetcher.py +++ b/src/infrastructure/fetcher/curl_cffi_fetcher.py @@ -17,12 +17,16 @@ import random import time from collections.abc import Sequence -from datetime import datetime, timezone +from datetime import UTC, datetime from curl_cffi.requests import AsyncSession from curl_cffi.requests.exceptions import ( ConnectionError as CurlConnectionError, +) +from curl_cffi.requests.exceptions import ( RequestException as CurlRequestException, +) +from curl_cffi.requests.exceptions import ( Timeout as CurlTimeout, ) @@ -168,7 +172,7 @@ async def fetch(self, request: FetchRequest) -> FetchResponse: headers=headers_out, body=body, elapsed_s=elapsed, - fetched_at=datetime.now(tz=timezone.utc), + fetched_at=datetime.now(tz=UTC), ) if status == 429: diff --git a/src/infrastructure/fetcher/httpx_fetcher.py b/src/infrastructure/fetcher/httpx_fetcher.py index f7367c8..0962435 100644 --- a/src/infrastructure/fetcher/httpx_fetcher.py +++ b/src/infrastructure/fetcher/httpx_fetcher.py @@ -13,7 +13,7 @@ from __future__ import annotations import time -from datetime import datetime, timezone +from datetime import UTC, datetime import httpx @@ -126,7 +126,7 @@ async def fetch(self, request: FetchRequest) -> FetchResponse: headers=headers_out, body=body, elapsed_s=elapsed, - fetched_at=datetime.now(tz=timezone.utc), + fetched_at=datetime.now(tz=UTC), ) if status == 429: diff --git a/src/infrastructure/matching/_normalize.py b/src/infrastructure/matching/_normalize.py index 2f18747..6105c6d 100644 --- a/src/infrastructure/matching/_normalize.py +++ b/src/infrastructure/matching/_normalize.py @@ -14,7 +14,6 @@ import re import unicodedata - _DROP_BRAND_PREFIXES = ("divers ", "divers- ", "DIVERS ") _PUNCT_RE = re.compile(r"[^\w\s]", re.UNICODE) _WS_RE = re.compile(r"\s+") diff --git a/src/infrastructure/matching/scorer.py b/src/infrastructure/matching/scorer.py index 7472e37..95bad47 100644 --- a/src/infrastructure/matching/scorer.py +++ b/src/infrastructure/matching/scorer.py @@ -14,7 +14,6 @@ from domain.enums import Confidence, CustomsMatchStatus - # Tuning knobs — kept in code so they ship as a single artifact. WEIGHTS: dict[str, float] = { "exact": 0.40, diff --git a/src/infrastructure/matching/sentence_transformer_embedder.py b/src/infrastructure/matching/sentence_transformer_embedder.py index f3c2f09..cf9f1b5 100644 --- a/src/infrastructure/matching/sentence_transformer_embedder.py +++ b/src/infrastructure/matching/sentence_transformer_embedder.py @@ -25,7 +25,6 @@ from infrastructure.observability.logging import get_logger - _MODEL_NAME = "sentence-transformers/paraphrase-multilingual-mpnet-base-v2" _MODEL_DIM = 768 diff --git a/src/infrastructure/parsers/__init__.py b/src/infrastructure/parsers/__init__.py index 9a7da57..2052c33 100644 --- a/src/infrastructure/parsers/__init__.py +++ b/src/infrastructure/parsers/__init__.py @@ -10,8 +10,8 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass -from typing import Callable from urllib.parse import urlparse from application.ports.parser import ( diff --git a/src/infrastructure/parsers/cigarpassion.py b/src/infrastructure/parsers/cigarpassion.py index a7bad7d..23d7500 100644 --- a/src/infrastructure/parsers/cigarpassion.py +++ b/src/infrastructure/parsers/cigarpassion.py @@ -24,7 +24,7 @@ import re from decimal import Decimal from typing import Any -from urllib.parse import urljoin, urlparse +from urllib.parse import urlparse from selectolax.parser import HTMLParser @@ -35,7 +35,6 @@ ) from infrastructure.observability.logging import get_logger - _log = get_logger("parser.cigarpassion") # Only follow product URLs that look like cigars; skip humidors, alcohols, diff --git a/src/infrastructure/persistence/models.py b/src/infrastructure/persistence/models.py index ec3b745..7911913 100644 --- a/src/infrastructure/persistence/models.py +++ b/src/infrastructure/persistence/models.py @@ -14,12 +14,12 @@ from typing import Any from uuid import UUID, uuid4 +from pgvector.sqlalchemy import Vector from sqlalchemy import ( ARRAY, Boolean, Date, DateTime, - Enum as SAEnum, ForeignKey, Integer, Numeric, @@ -29,7 +29,9 @@ Uuid, func, ) -from pgvector.sqlalchemy import Vector +from sqlalchemy import ( + Enum as SAEnum, +) from sqlalchemy.dialects.postgresql import CITEXT, INET, JSONB from sqlalchemy.orm import Mapped, mapped_column, relationship diff --git a/src/infrastructure/persistence/repositories/brand.py b/src/infrastructure/persistence/repositories/brand.py index c3a91d9..54689c2 100644 --- a/src/infrastructure/persistence/repositories/brand.py +++ b/src/infrastructure/persistence/repositories/brand.py @@ -15,7 +15,6 @@ from infrastructure.persistence.mappers import ( apply_brand_to_model, brand_to_domain, - brand_to_model, ) from infrastructure.persistence.models import BrandModel diff --git a/src/infrastructure/persistence/repositories/cigar.py b/src/infrastructure/persistence/repositories/cigar.py index 776e0a2..afcaedb 100644 --- a/src/infrastructure/persistence/repositories/cigar.py +++ b/src/infrastructure/persistence/repositories/cigar.py @@ -21,9 +21,7 @@ apply_cigar_to_model, blend_to_model, cigar_line_to_domain, - cigar_line_to_model, cigar_to_domain, - cigar_to_model, ) from infrastructure.persistence.models import BrandModel, CigarLineModel, CigarModel diff --git a/src/infrastructure/persistence/repositories/customs.py b/src/infrastructure/persistence/repositories/customs.py index 0f66d1c..002cf37 100644 --- a/src/infrastructure/persistence/repositories/customs.py +++ b/src/infrastructure/persistence/repositories/customs.py @@ -6,7 +6,7 @@ from __future__ import annotations from collections.abc import Sequence -from datetime import date, datetime +from datetime import UTC, date, datetime from uuid import UUID from sqlalchemy import select @@ -34,7 +34,6 @@ CustomsSourceModel, ) - # --------------------------------------------------------------------------- # CustomsSource # --------------------------------------------------------------------------- @@ -239,7 +238,8 @@ async def list_by_source( return [customs_publication_to_domain(m) for m in result.scalars().all()] async def count_by_source(self, source_id: UUID) -> int: - from sqlalchemy import func as _func, select as _select + from sqlalchemy import func as _func + from sqlalchemy import select as _select result = await self._session.execute( _select(_func.count()) @@ -529,9 +529,8 @@ async def apply_human_decision( notes: str | None = None, when: datetime | None = None, ) -> CigarCustomsMatch: - from datetime import timezone as _tz - when = when or datetime.now(tz=_tz.utc) + when = when or datetime.now(tz=UTC) new_status = ( CustomsMatchStatus.HUMAN_ACCEPTED if accepted else CustomsMatchStatus.HUMAN_REJECTED ) diff --git a/src/infrastructure/persistence/repositories/matching.py b/src/infrastructure/persistence/repositories/matching.py index a98f062..4d64821 100644 --- a/src/infrastructure/persistence/repositories/matching.py +++ b/src/infrastructure/persistence/repositories/matching.py @@ -21,7 +21,6 @@ CustomsPriceEntryModel, ) - # Canonical SQL that builds the embedding text for a cigar. Kept here (not in # the Python normalization layer) because it lets us push the work into the # DB and avoids materializing every row in Python when we only need the text. diff --git a/src/infrastructure/persistence/repositories/media.py b/src/infrastructure/persistence/repositories/media.py index 5320f6c..2d99bc9 100644 --- a/src/infrastructure/persistence/repositories/media.py +++ b/src/infrastructure/persistence/repositories/media.py @@ -6,7 +6,7 @@ from __future__ import annotations from collections.abc import Sequence -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import UUID from sqlalchemy import select @@ -75,7 +75,7 @@ async def mark_status( if media_blob_hash is not None: existing.media_blob_hash = media_blob_hash if status == MediaStatus.OK and existing.downloaded_at is None: - existing.downloaded_at = datetime.now(tz=timezone.utc) + existing.downloaded_at = datetime.now(tz=UTC) await self._session.flush() await self._session.refresh(existing) return media_to_domain(existing) diff --git a/src/infrastructure/workers/jobs.py b/src/infrastructure/workers/jobs.py index 5ab9ddd..9c424f7 100644 --- a/src/infrastructure/workers/jobs.py +++ b/src/infrastructure/workers/jobs.py @@ -13,17 +13,16 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from uuid import UUID from application.ports.fetcher import FetchError, FetchRequest -from application.use_cases.build_embeddings import BuildEmbeddingsUseCase, Target +from application.use_cases.build_embeddings import BuildEmbeddingsUseCase from application.use_cases.ingest_customs_publication import ( IngestCustomsPublicationUseCase, ) from application.use_cases.ingest_product import ( - IngestOutcome, IngestProductUrlUseCase, ) from application.use_cases.match_cigar_to_customs import MatchCigarToCustomsUseCase @@ -240,7 +239,7 @@ async def download_media_job(ctx: dict[str, Any], asset_id: str) -> dict[str, An # Step 4: hash + upsert MediaBlob (idempotent on content_hash) content_hash = blake3_hex(normalized.data) - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) new_blob = MediaBlob( content_hash=content_hash, storage_key=f"{content_hash[:2]}/{content_hash}.{normalized.ext}", diff --git a/src/infrastructure/workers/worker.py b/src/infrastructure/workers/worker.py index a591f84..e142c0a 100644 --- a/src/infrastructure/workers/worker.py +++ b/src/infrastructure/workers/worker.py @@ -28,15 +28,15 @@ import infrastructure.customs # noqa: F401 from infrastructure.config import get_settings from infrastructure.fetcher.curl_cffi_fetcher import ( - CurlCffiFetcher, DEFAULT_IMPERSONATE_POOL, + CurlCffiFetcher, ) -from infrastructure.media.seaweed_storage import SeaweedS3Storage -from infrastructure.observability.logging import configure_logging, get_logger -from infrastructure.persistence.session import build_engine, build_session_factory from infrastructure.matching.sentence_transformer_embedder import ( SentenceTransformerEmbedder, ) +from infrastructure.media.seaweed_storage import SeaweedS3Storage +from infrastructure.observability.logging import configure_logging, get_logger +from infrastructure.persistence.session import build_engine, build_session_factory from infrastructure.workers.jobs import ( compute_embeddings_job, crawl_listing_job, diff --git a/src/presentation/api/dependencies.py b/src/presentation/api/dependencies.py index e360329..022a339 100644 --- a/src/presentation/api/dependencies.py +++ b/src/presentation/api/dependencies.py @@ -16,14 +16,14 @@ from fastapi import Depends, HTTPException, Query, Request, status from fastapi.security import OAuth2PasswordBearer -from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, AsyncSession +from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker from domain.entities.api_user import ApiUser -from infrastructure.config.settings import ApiSettings, get_settings as _build_settings +from infrastructure.config.settings import ApiSettings +from infrastructure.config.settings import get_settings as _build_settings from infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork from presentation.api.security.jwt import JwtError, decode_access_token - # --------------------------------------------------------------------------- # Settings # --------------------------------------------------------------------------- diff --git a/src/presentation/api/errors.py b/src/presentation/api/errors.py index fb93910..4bb82cb 100644 --- a/src/presentation/api/errors.py +++ b/src/presentation/api/errors.py @@ -5,7 +5,6 @@ from __future__ import annotations -from typing import Any from uuid import uuid4 from fastapi import FastAPI, HTTPException, Request, status @@ -16,7 +15,6 @@ from infrastructure.observability.logging import get_logger - _log = get_logger("api.errors") diff --git a/src/presentation/api/hypermedia.py b/src/presentation/api/hypermedia.py index 5b40fd3..a9b2aac 100644 --- a/src/presentation/api/hypermedia.py +++ b/src/presentation/api/hypermedia.py @@ -12,7 +12,6 @@ from presentation.api.schemas.base import Links, PageLinks - _API_PREFIX = "/api/v1" diff --git a/src/presentation/api/main.py b/src/presentation/api/main.py index 0d6dec9..3ced455 100644 --- a/src/presentation/api/main.py +++ b/src/presentation/api/main.py @@ -13,7 +13,8 @@ import contextlib from collections.abc import AsyncIterator -from importlib.metadata import PackageNotFoundError, version as _pkg_version +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as _pkg_version from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -28,7 +29,6 @@ from presentation.api.middleware import register_middleware from presentation.api.rate_limit import register_rate_limit - _log = get_logger("api.main") diff --git a/src/presentation/api/middleware.py b/src/presentation/api/middleware.py index 8faa9e2..1ee3912 100644 --- a/src/presentation/api/middleware.py +++ b/src/presentation/api/middleware.py @@ -8,7 +8,7 @@ from __future__ import annotations import time -from typing import Callable +from collections.abc import Callable from uuid import uuid4 import structlog @@ -19,7 +19,6 @@ from infrastructure.config.settings import ApiSettings from infrastructure.observability.logging import get_logger - _REQUEST_ID_HEADER = "X-Request-Id" _log = get_logger("api.access") diff --git a/src/presentation/api/rate_limit.py b/src/presentation/api/rate_limit.py index 3d19e1b..4404694 100644 --- a/src/presentation/api/rate_limit.py +++ b/src/presentation/api/rate_limit.py @@ -24,7 +24,6 @@ from presentation.api.errors import _build # re-use the RFC 7807 builder from presentation.api.security.jwt import JwtError, decode_access_token - _log = get_logger("api.rate_limit") diff --git a/src/presentation/api/routers/auth_web.py b/src/presentation/api/routers/auth_web.py index e13fc7e..5f42e24 100644 --- a/src/presentation/api/routers/auth_web.py +++ b/src/presentation/api/routers/auth_web.py @@ -33,7 +33,6 @@ revoke_refresh_token, ) - router = APIRouter(prefix="/auth", tags=["Auth (Web)"]) @@ -192,4 +191,4 @@ async def logout( # cookie-clearing Set-Cookie header survives next to the 204. clear_refresh_cookie(response, settings=settings, request=request) response.status_code = status.HTTP_204_NO_CONTENT - return None + return diff --git a/src/presentation/api/routers/brands.py b/src/presentation/api/routers/brands.py index a05c1ec..00bf08a 100644 --- a/src/presentation/api/routers/brands.py +++ b/src/presentation/api/routers/brands.py @@ -22,7 +22,6 @@ from presentation.api.schemas.brand import BrandResponse from presentation.api.schemas.line import LineResponse - router = APIRouter(prefix="/brands", tags=["Brands"]) diff --git a/src/presentation/api/routers/cigars.py b/src/presentation/api/routers/cigars.py index 0479849..742621e 100644 --- a/src/presentation/api/routers/cigars.py +++ b/src/presentation/api/routers/cigars.py @@ -8,7 +8,7 @@ from typing import Annotated from uuid import UUID -from fastapi import APIRouter, HTTPException, Path, Query, Request, Response, status +from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request, Response, status from application.ports.cigar_repository import CigarFilters, CigarSort from domain.entities.cigar import Cigar @@ -31,8 +31,6 @@ CigarSummary, ) from presentation.api.schemas.search import SearchHit, SearchResponse -from fastapi import Depends - router = APIRouter(prefix="/cigars", tags=["Cigars"]) diff --git a/src/presentation/api/routers/customs_entries.py b/src/presentation/api/routers/customs_entries.py index 3256101..2ae8862 100644 --- a/src/presentation/api/routers/customs_entries.py +++ b/src/presentation/api/routers/customs_entries.py @@ -15,7 +15,6 @@ from presentation.api.hypermedia import make_links from presentation.api.schemas.customs import CustomsEntryResponse - router = APIRouter(prefix="/customs-entries", tags=["Customs Entries"]) diff --git a/src/presentation/api/routers/customs_publications.py b/src/presentation/api/routers/customs_publications.py index 87bbed8..c63d0a0 100644 --- a/src/presentation/api/routers/customs_publications.py +++ b/src/presentation/api/routers/customs_publications.py @@ -23,7 +23,6 @@ CustomsPublicationResponse, ) - router = APIRouter(prefix="/customs-publications", tags=["Customs Publications"]) diff --git a/src/presentation/api/routers/customs_sources.py b/src/presentation/api/routers/customs_sources.py index c58db6e..00a8409 100644 --- a/src/presentation/api/routers/customs_sources.py +++ b/src/presentation/api/routers/customs_sources.py @@ -20,7 +20,6 @@ from presentation.api.schemas.base import PaginatedResponse from presentation.api.schemas.customs import CustomsSourceResponse - router = APIRouter(prefix="/customs-sources", tags=["Customs Sources"]) diff --git a/src/presentation/api/routers/jobs.py b/src/presentation/api/routers/jobs.py index 705f760..9f6a8c4 100644 --- a/src/presentation/api/routers/jobs.py +++ b/src/presentation/api/routers/jobs.py @@ -13,7 +13,6 @@ from presentation.api.hypermedia import make_links from presentation.api.schemas.job import JobResponse - router = APIRouter(prefix="/jobs", tags=["Jobs"]) diff --git a/src/presentation/api/routers/lines.py b/src/presentation/api/routers/lines.py index efe7c1a..a77ddc1 100644 --- a/src/presentation/api/routers/lines.py +++ b/src/presentation/api/routers/lines.py @@ -16,7 +16,6 @@ from presentation.api.schemas.cigar import CigarSummary from presentation.api.schemas.line import LineResponse - router = APIRouter(prefix="/lines", tags=["Lines"]) diff --git a/src/presentation/api/routers/match_jobs.py b/src/presentation/api/routers/match_jobs.py index 17899d7..44301dd 100644 --- a/src/presentation/api/routers/match_jobs.py +++ b/src/presentation/api/routers/match_jobs.py @@ -14,7 +14,6 @@ from presentation.api.schemas.base import JobAcceptedResponse from presentation.api.schemas.job import MatchJobRequest - router = APIRouter(prefix="/match-jobs", tags=["Jobs"]) diff --git a/src/presentation/api/routers/matches.py b/src/presentation/api/routers/matches.py index f54b001..cb328a5 100644 --- a/src/presentation/api/routers/matches.py +++ b/src/presentation/api/routers/matches.py @@ -26,7 +26,6 @@ from presentation.api.schemas.base import PaginatedResponse from presentation.api.schemas.match import MatchPatchRequest, MatchResponse - router = APIRouter(prefix="/matches", tags=["Matches"]) diff --git a/src/presentation/api/routers/media.py b/src/presentation/api/routers/media.py index 9a8ba46..3df58d6 100644 --- a/src/presentation/api/routers/media.py +++ b/src/presentation/api/routers/media.py @@ -17,7 +17,6 @@ from presentation.api.hypermedia import make_links from presentation.api.schemas.media import MediaAssetResponse - router = APIRouter(tags=["Media"]) diff --git a/src/presentation/api/routers/oauth.py b/src/presentation/api/routers/oauth.py index 261f34f..bb924e3 100644 --- a/src/presentation/api/routers/oauth.py +++ b/src/presentation/api/routers/oauth.py @@ -22,7 +22,6 @@ revoke_refresh_token, ) - router = APIRouter(prefix="/oauth", tags=["OAuth"]) diff --git a/src/presentation/api/routers/packages.py b/src/presentation/api/routers/packages.py index aad2cf5..aa54f05 100644 --- a/src/presentation/api/routers/packages.py +++ b/src/presentation/api/routers/packages.py @@ -14,7 +14,6 @@ from presentation.api.hypermedia import make_links from presentation.api.schemas.package import PackageResponse - router = APIRouter(prefix="/cigars/{key}/packages", tags=["Packages"]) diff --git a/src/presentation/api/routers/refresh_jobs.py b/src/presentation/api/routers/refresh_jobs.py index dc06e1a..cb3c69f 100644 --- a/src/presentation/api/routers/refresh_jobs.py +++ b/src/presentation/api/routers/refresh_jobs.py @@ -13,7 +13,6 @@ from presentation.api.hypermedia import make_links from presentation.api.schemas.base import JobAcceptedResponse - router = APIRouter(prefix="/customs-sources/{code}/refresh-jobs", tags=["Jobs"]) diff --git a/src/presentation/api/routers/system.py b/src/presentation/api/routers/system.py index 97e9c95..57a89c5 100644 --- a/src/presentation/api/routers/system.py +++ b/src/presentation/api/routers/system.py @@ -19,7 +19,6 @@ VersionResponse, ) - router = APIRouter(tags=["System"]) @@ -125,7 +124,8 @@ async def health( summary="Build / schema version metadata.", ) async def version_endpoint() -> VersionResponse: - from importlib.metadata import PackageNotFoundError, version as pkg_version + from importlib.metadata import PackageNotFoundError + from importlib.metadata import version as pkg_version try: v = pkg_version("cigars") diff --git a/src/presentation/api/routers/users.py b/src/presentation/api/routers/users.py index 12ed76a..6fa77f6 100644 --- a/src/presentation/api/routers/users.py +++ b/src/presentation/api/routers/users.py @@ -11,7 +11,6 @@ from presentation.api.hypermedia import make_links from presentation.api.schemas.user import UserResponse - router = APIRouter(prefix="/me", tags=["Users"]) diff --git a/src/presentation/api/schemas/base.py b/src/presentation/api/schemas/base.py index 5b6b7cd..6085eb3 100644 --- a/src/presentation/api/schemas/base.py +++ b/src/presentation/api/schemas/base.py @@ -9,7 +9,6 @@ from pydantic import BaseModel, ConfigDict, Field - T = TypeVar("T") diff --git a/src/presentation/api/security/cookies.py b/src/presentation/api/security/cookies.py index 533e7fd..7d7d199 100644 --- a/src/presentation/api/security/cookies.py +++ b/src/presentation/api/security/cookies.py @@ -15,7 +15,6 @@ from infrastructure.config.settings import ApiSettings - REFRESH_COOKIE_NAME = "cigars_refresh" COOKIE_PATH = "/api/v1/auth" diff --git a/src/presentation/api/security/jwt.py b/src/presentation/api/security/jwt.py index 0dcab86..709fb06 100644 --- a/src/presentation/api/security/jwt.py +++ b/src/presentation/api/security/jwt.py @@ -19,14 +19,13 @@ import hashlib import secrets from dataclasses import dataclass -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from uuid import UUID, uuid4 import jwt from infrastructure.config.settings import ApiSettings - _REFRESH_TOKEN_BYTES = 32 @@ -53,7 +52,7 @@ def encode_access_token( now: datetime | None = None, ) -> tuple[str, datetime]: """Mint an access token and return ``(token, expires_at)``.""" - now = now or datetime.now(tz=timezone.utc) + now = now or datetime.now(tz=UTC) expires_at = now + timedelta(seconds=settings.access_token_expire_seconds) payload = { "sub": str(user_id), @@ -82,8 +81,8 @@ def decode_access_token(token: str, *, settings: ApiSettings) -> AccessTokenClai sub=UUID(payload["sub"]), scope=tuple(payload.get("scope") or ()), jti=UUID(payload["jti"]), - iat=datetime.fromtimestamp(payload["iat"], tz=timezone.utc), - exp=datetime.fromtimestamp(payload["exp"], tz=timezone.utc), + iat=datetime.fromtimestamp(payload["iat"], tz=UTC), + exp=datetime.fromtimestamp(payload["exp"], tz=UTC), ) except (KeyError, ValueError, TypeError) as exc: raise JwtError(f"malformed claims: {exc}") from exc @@ -105,5 +104,5 @@ def hash_refresh_token(plain: str) -> str: def refresh_token_expires_at(settings: ApiSettings, *, now: datetime | None = None) -> datetime: - now = now or datetime.now(tz=timezone.utc) + now = now or datetime.now(tz=UTC) return now + timedelta(seconds=settings.refresh_token_expire_seconds) diff --git a/src/presentation/api/security/oauth.py b/src/presentation/api/security/oauth.py index 75338d0..4990c68 100644 --- a/src/presentation/api/security/oauth.py +++ b/src/presentation/api/security/oauth.py @@ -18,7 +18,7 @@ from __future__ import annotations from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from domain.entities.api_user import ApiUser, RefreshToken from infrastructure.config.settings import ApiSettings @@ -30,8 +30,7 @@ hash_refresh_token, refresh_token_expires_at, ) -from presentation.api.security.password import needs_rehash, verify_password, hash_password - +from presentation.api.security.password import hash_password, needs_rehash, verify_password _log = get_logger("api.oauth") @@ -72,7 +71,7 @@ async def password_grant( now: datetime | None = None, ) -> IssuedTokens: """Resource Owner Password Credentials grant.""" - now = now or datetime.now(tz=timezone.utc) + now = now or datetime.now(tz=UTC) user = await uow.api_users.get_by_email(email) if user is None or not user.is_active: raise OAuthError("invalid_grant", "invalid email or password") @@ -114,7 +113,7 @@ async def refresh_token_grant( now: datetime | None = None, ) -> IssuedTokens: """Exchange a valid refresh token for a new pair (rotation).""" - now = now or datetime.now(tz=timezone.utc) + now = now or datetime.now(tz=UTC) token_hash = hash_refresh_token(refresh_token) stored = await uow.refresh_tokens.find_by_hash(token_hash) @@ -159,7 +158,7 @@ async def revoke_refresh_token( now: datetime | None = None, ) -> bool: """RFC 7009 revocation. Returns True if a token was actually revoked.""" - now = now or datetime.now(tz=timezone.utc) + now = now or datetime.now(tz=UTC) token_hash = hash_refresh_token(refresh_token) stored = await uow.refresh_tokens.find_by_hash(token_hash) if stored is None or stored.revoked_at is not None: diff --git a/src/presentation/api/security/password.py b/src/presentation/api/security/password.py index 9f1459e..071d58f 100644 --- a/src/presentation/api/security/password.py +++ b/src/presentation/api/security/password.py @@ -12,7 +12,6 @@ from argon2 import PasswordHasher from argon2.exceptions import InvalidHashError, VerifyMismatchError - _hasher = PasswordHasher() diff --git a/src/presentation/cli/__main__.py b/src/presentation/cli/__main__.py index 58e6230..44c2b27 100644 --- a/src/presentation/cli/__main__.py +++ b/src/presentation/cli/__main__.py @@ -6,7 +6,8 @@ from __future__ import annotations import asyncio -from importlib.metadata import PackageNotFoundError, version as pkg_version +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as pkg_version import typer from rich.console import Console @@ -15,7 +16,7 @@ from application.use_cases.crawl_listing import CrawlListingUseCase from application.use_cases.ingest_product import IngestProductUrlUseCase from infrastructure.config import get_settings -from infrastructure.fetcher.curl_cffi_fetcher import CurlCffiFetcher, DEFAULT_IMPERSONATE_POOL +from infrastructure.fetcher.curl_cffi_fetcher import DEFAULT_IMPERSONATE_POOL, CurlCffiFetcher from infrastructure.observability.logging import configure_logging, get_logger from infrastructure.parsers import ( UnknownDomainError, diff --git a/tests/application/matching/test_match_cigar_to_customs.py b/tests/application/matching/test_match_cigar_to_customs.py index 0c591c0..f40b935 100644 --- a/tests/application/matching/test_match_cigar_to_customs.py +++ b/tests/application/matching/test_match_cigar_to_customs.py @@ -10,15 +10,13 @@ import hashlib from collections.abc import Sequence -from datetime import date, datetime, timezone +from datetime import UTC, date, datetime from decimal import Decimal from typing import ClassVar from uuid import uuid4 -import pytest import pytest_asyncio -from application.ports.embedder import IEmbedder from application.use_cases.build_embeddings import BuildEmbeddingsUseCase from application.use_cases.match_cigar_to_customs import MatchCigarToCustomsUseCase from domain.entities.brand import Brand @@ -36,7 +34,6 @@ ) from infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork - _DIM = 768 @@ -110,7 +107,7 @@ async def seeded_uow(session_factory, clean_db): ) ) - now = datetime.now(tz=timezone.utc) + now = datetime.now(tz=UTC) entries = [ CustomsPriceEntry( publication_id=publication.id, @@ -223,9 +220,10 @@ async def test_human_status_is_preserved_across_rematch(session_factory, seeded_ # Replace via upsert directly on the model layer is tricky; we use the # repository to set the status (since add() doesn't update, we mutate # via raw SQL through the session for the test). - from infrastructure.persistence.models import CigarCustomsMatchModel from sqlalchemy import update + from infrastructure.persistence.models import CigarCustomsMatchModel + await uow._session.execute( # type: ignore[attr-defined] update(CigarCustomsMatchModel) .where(CigarCustomsMatchModel.id == locked_id) diff --git a/tests/application/test_download_media_job.py b/tests/application/test_download_media_job.py index 4c598a5..0151655 100644 --- a/tests/application/test_download_media_job.py +++ b/tests/application/test_download_media_job.py @@ -11,7 +11,7 @@ from __future__ import annotations import io -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from uuid import UUID @@ -55,7 +55,7 @@ async def fetch(self, req: FetchRequest) -> FetchResponse: headers={"content-type": "image/png"}, body=v, elapsed_s=0.01, - fetched_at=datetime.now(tz=timezone.utc), + fetched_at=datetime.now(tz=UTC), ) async def aclose(self) -> None: diff --git a/tests/application/test_ingest_customs_publication_usecase.py b/tests/application/test_ingest_customs_publication_usecase.py index 5564a9e..733356e 100644 --- a/tests/application/test_ingest_customs_publication_usecase.py +++ b/tests/application/test_ingest_customs_publication_usecase.py @@ -3,7 +3,7 @@ # See LICENSE for terms; COMMERCIAL_LICENSE.md for commercial use. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path import pytest @@ -34,7 +34,7 @@ async def fetch(self, request: FetchRequest) -> FetchResponse: headers={"content-type": "text/html; charset=utf-8"}, body=self._body, elapsed_s=0.01, - fetched_at=datetime.now(tz=timezone.utc), + fetched_at=datetime.now(tz=UTC), ) async def aclose(self) -> None: diff --git a/tests/application/test_ingest_product_usecase.py b/tests/application/test_ingest_product_usecase.py index 847bd42..a83ef2e 100644 --- a/tests/application/test_ingest_product_usecase.py +++ b/tests/application/test_ingest_product_usecase.py @@ -3,7 +3,7 @@ # See LICENSE for terms; COMMERCIAL_LICENSE.md for commercial use. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path import pytest @@ -36,7 +36,7 @@ async def fetch(self, request: FetchRequest) -> FetchResponse: headers={"content-type": "text/html; charset=utf-8"}, body=self._body, elapsed_s=0.01, - fetched_at=datetime.now(tz=timezone.utc), + fetched_at=datetime.now(tz=UTC), ) async def aclose(self) -> None: diff --git a/tests/application/test_packaging_convergence.py b/tests/application/test_packaging_convergence.py index 8ad869a..c9aa1f0 100644 --- a/tests/application/test_packaging_convergence.py +++ b/tests/application/test_packaging_convergence.py @@ -4,7 +4,7 @@ from __future__ import annotations import re -from datetime import datetime, timezone +from datetime import UTC, datetime from decimal import Decimal from pathlib import Path @@ -88,7 +88,7 @@ async def fetch(self, request: FetchRequest) -> FetchResponse: headers={"content-type": "text/html; charset=utf-8"}, body=self._body, elapsed_s=0.01, - fetched_at=datetime.now(tz=timezone.utc), + fetched_at=datetime.now(tz=UTC), ) async def aclose(self) -> None: diff --git a/tests/application/test_refresh_customs_source_usecase.py b/tests/application/test_refresh_customs_source_usecase.py index 2ef1811..aa20edb 100644 --- a/tests/application/test_refresh_customs_source_usecase.py +++ b/tests/application/test_refresh_customs_source_usecase.py @@ -3,7 +3,7 @@ # See LICENSE for terms; COMMERCIAL_LICENSE.md for commercial use. from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path import pytest @@ -32,7 +32,7 @@ async def fetch(self, request: FetchRequest) -> FetchResponse: headers={"content-type": "text/html; charset=utf-8"}, body=self._body, elapsed_s=0.01, - fetched_at=datetime.now(tz=timezone.utc), + fetched_at=datetime.now(tz=UTC), ) async def aclose(self) -> None: diff --git a/tests/domain/test_entities.py b/tests/domain/test_entities.py index 5cc4340..ea5f560 100644 --- a/tests/domain/test_entities.py +++ b/tests/domain/test_entities.py @@ -3,7 +3,7 @@ # See LICENSE for terms; COMMERCIAL_LICENSE.md for commercial use. from __future__ import annotations -from datetime import date, datetime, timezone +from datetime import UTC, date, datetime from decimal import Decimal from uuid import uuid4 @@ -97,7 +97,7 @@ def test_customs_price_entry_requires_publication() -> None: unit_price=Decimal("18.50"), raw_brand_label="COHIBA", raw_product_label="ROBUSTO", - extracted_at=datetime(2024, 3, 1, tzinfo=timezone.utc), + extracted_at=datetime(2024, 3, 1, tzinfo=UTC), extractor_version="legifrance-html-1.0", ) @@ -110,7 +110,7 @@ def test_customs_match_requires_score_in_range() -> None: match_method=MatchMethod.EXACT, score=Decimal("1.5"), # > 1.0 confidence=Confidence.HIGH, - matched_at=datetime(2024, 3, 1, tzinfo=timezone.utc), + matched_at=datetime(2024, 3, 1, tzinfo=UTC), matched_by="system", ) @@ -126,7 +126,7 @@ def test_customs_entry_full() -> None: raw_brand_label="COHIBA", raw_product_label="ROBUSTO", pack_size=25, - extracted_at=datetime(2024, 3, 1, tzinfo=timezone.utc), + extracted_at=datetime(2024, 3, 1, tzinfo=UTC), extractor_version="legifrance-html-1.0", ) assert e.unit_price == Decimal("18.50") diff --git a/tests/e2e/web_smoke.py b/tests/e2e/web_smoke.py index 3c885d7..380cc00 100644 --- a/tests/e2e/web_smoke.py +++ b/tests/e2e/web_smoke.py @@ -27,7 +27,6 @@ from patchright.async_api import async_playwright - WEB_URL = os.environ.get("E2E_WEB_URL", "http://127.0.0.1:3000") ADMIN_EMAIL = os.environ.get("E2E_ADMIN_EMAIL", "admin@example.com") ADMIN_PASSWORD = os.environ.get("E2E_ADMIN_PASSWORD", "admin-dev-pass-2026") diff --git a/tests/infrastructure/customs/test_douane_opendata_discovery.py b/tests/infrastructure/customs/test_douane_opendata_discovery.py index 7843d74..8fa9b58 100644 --- a/tests/infrastructure/customs/test_douane_opendata_discovery.py +++ b/tests/infrastructure/customs/test_douane_opendata_discovery.py @@ -9,7 +9,6 @@ DouaneOpenDataDiscovery, ) - _INDEX_HTML = """