Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
6 changes: 3 additions & 3 deletions src/application/ports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@
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,
IProductParser,
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
Expand Down
1 change: 0 additions & 1 deletion src/application/ports/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

from pydantic import BaseModel, ConfigDict, Field


# ---------------------------------------------------------------------------
# Listing parser
# ---------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions src/application/services/extraction_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/application/use_cases/build_embeddings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
from infrastructure.observability.logging import get_logger
from infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork


Target = Literal["cigar", "customs", "all"]


Expand Down
3 changes: 2 additions & 1 deletion src/application/use_cases/crawl_listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/application/use_cases/hybrid_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from infrastructure.matching._normalize import normalize
from infrastructure.persistence.unit_of_work import SqlAlchemyUnitOfWork


_RRF_CONSTANT = 60


Expand Down
12 changes: 6 additions & 6 deletions src/application/use_cases/ingest_customs_publication.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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()
Expand All @@ -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(
Expand Down
4 changes: 2 additions & 2 deletions src/application/use_cases/ingest_product.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
5 changes: 2 additions & 3 deletions src/application/use_cases/match_cigar_to_customs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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"


Expand Down Expand Up @@ -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:
Expand Down
6 changes: 3 additions & 3 deletions src/application/use_cases/refresh_customs_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
)
Expand Down
2 changes: 0 additions & 2 deletions src/infrastructure/customs/discovery/douane_opendata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

from application.ports.customs_discovery import (
DiscoveredPublication,
ICustomsDiscoveryAdapter,
)
from infrastructure.customs._date_fr import parse_french_date

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/infrastructure/customs/discovery/legifrance_jorf.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

from application.ports.customs_discovery import (
DiscoveredPublication,
ICustomsDiscoveryAdapter,
)
from infrastructure.customs._date_fr import parse_french_date

Expand Down
2 changes: 0 additions & 2 deletions src/infrastructure/customs/extractors/douane_ods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions src/infrastructure/customs/extractors/legifrance_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 0 additions & 1 deletion src/infrastructure/customs/extractors/ofdf_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 7 additions & 3 deletions src/infrastructure/fetcher/browser_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
Loading
Loading