mirror of
https://github.com/PostHog/posthog.git
synced 2026-10-26 09:51:43 +08:00
953 lines
45 KiB
TOML
953 lines
45 KiB
TOML
|
|
|
|
[project]
|
|
name = "posthog"
|
|
version = "0.0.0"
|
|
requires-python = "==3.14.7" # Pin full version to control Python upgrades explicitly
|
|
# Prefer ~= (compat) over >= (floor). Only pin exact versions (==) when necessary.
|
|
#
|
|
# ~= signals to others (security, devex, dependabot) that deps can be updated within
|
|
# that range without pulling the team in. Unbounded >= gives no such signal.
|
|
#
|
|
dependencies = [
|
|
"aioboto3==15.4.0",
|
|
"aiohttp==3.13.5",
|
|
"aiokafka~=0.13.0",
|
|
"anthropic==0.80.0",
|
|
"asyncstdlib==3.13.1",
|
|
"beautifulsoup4==4.12.3",
|
|
"boto3==1.40.49",
|
|
"brotli==1.2.1",
|
|
"celery==5.6.3",
|
|
"celery-redbeat==2.3.3",
|
|
"certifi==2026.10.19",
|
|
"clickhouse-connect==0.10.0",
|
|
"clickhouse-driver==0.2.10",
|
|
"clickhouse-pool==0.5.3",
|
|
"circular-dict==1.9",
|
|
"cryptography==46.0.7",
|
|
"PyNaCl==1.5.0",
|
|
"dagster==1.12.11",
|
|
"dagster-cloud==1.12.11",
|
|
"dagster-aws==0.28.11",
|
|
"dagster-celery==0.28.11",
|
|
"dagster-docker==0.28.11",
|
|
"dagster-k8s==0.28.11",
|
|
"dagster-pipes==1.12.11",
|
|
"dagster-postgres==0.28.11",
|
|
"dagster-slack==0.28.11",
|
|
"dagster-webserver==1.12.11",
|
|
"deltalake[pyarrow]==1.6.1",
|
|
"dj-database-url~=3.0.0",
|
|
"django~=5.2.0",
|
|
"django-axes[ipware]==8.3.1",
|
|
"django-cors-headers~=4.9.0",
|
|
"django-extensions~=4.1.0",
|
|
"django-filter~=26.1",
|
|
"django-loginas==0.3.14",
|
|
"django-prometheus~=2.5.0",
|
|
"django-redis==7.0.0",
|
|
"django-structlog[celery]~=10.1.0",
|
|
"django-two-factor-auth~=1.18.0",
|
|
"djangorestframework~=3.17.2",
|
|
"djangorestframework-csv~=3.0.2",
|
|
"djangorestframework-dataclasses~=1.4.0",
|
|
"dlt==1.18.2",
|
|
"dnspython==2.6.1",
|
|
"drf-exceptions-hog==0.4.0",
|
|
"drf-extensions~=0.8.0",
|
|
"drf-spectacular~=0.30.0",
|
|
"drf-spectacular-sidecar~=2026.9.1",
|
|
"geoip2==4.6.0",
|
|
"google-ads==31.2.0",
|
|
"google-cloud-bigquery==3.40.1",
|
|
"google-cloud-bigquery-storage==2.32.0",
|
|
"google-cloud-iam==2.21",
|
|
"google-genai==1.75.0",
|
|
# 2.8+ gives each worker its own SO_REUSEPORT socket on Linux. The query pool runs
|
|
# bin/granian_shared_socket.py to keep one shared socket; its test must pass on any bump.
|
|
"granian[uvloop,reload,pname]==2.8.3",
|
|
"grimp~=3.14",
|
|
"grpcio~=1.84.1",
|
|
"gspread==6.2.1",
|
|
"hogql-parser==1.3.85",
|
|
"hogql-parser-rs==1.3.109", # built from rust/hogql/parser via the build-hogql-parser-rs workflow + published to PyPI; rebuild locally with `uv pip install -e rust/hogql/parser` when iterating
|
|
"humanize~=4.12.3",
|
|
"infi-clickhouse-orm",
|
|
"ipython~=9.3.0",
|
|
"jsonref==1.1.0",
|
|
"kafka-python==2.3.0",
|
|
"kombu==5.6.2",
|
|
"jsonpatch~=1.33",
|
|
"langchain~=0.3.27",
|
|
"langchain-community~=0.3.31",
|
|
"langchain-core~=0.3.81",
|
|
"langchain-openai~=0.3.35",
|
|
"langchain-anthropic~=0.3.22",
|
|
"langgraph==0.4.10",
|
|
"langgraph-checkpoint==4.1.1",
|
|
"lxml==6.0.2",
|
|
"lzstring==1.0.4",
|
|
"markdown-it-py~=3.0.0",
|
|
"mcp~=1.26.0",
|
|
"mimesis==5.2.1",
|
|
"mistralai==1.6.0",
|
|
"more-itertools==9.0.0",
|
|
"nanoid==2.0.0",
|
|
"natsort==8.4.0",
|
|
"nh3==0.3.5",
|
|
"numpy~=2.3.2",
|
|
"openai==2.30.0",
|
|
"openpyxl==3.1.5",
|
|
"orjson==3.11.9",
|
|
"ormsgpack~=1.12",
|
|
"pandas~=2.3.3",
|
|
"paramiko~=3.5.0",
|
|
"pillow==12.3.0",
|
|
"protobuf~=6.31.1",
|
|
"posthoganalytics==7.75.11",
|
|
"polars==1.37.1",
|
|
"psycopg2-binary==2.9.11",
|
|
"psycopg[binary]==3.2.10",
|
|
"pyarrow==23.0.1",
|
|
"pydantic~=2.12.0",
|
|
"pypdf==6.10.2",
|
|
"pyod==2.0.7",
|
|
"pyjwt~=2.12.1",
|
|
"pymssql==2.3.8",
|
|
"pymysql==1.1.1",
|
|
"pymongo==4.15.2",
|
|
"pyroscope-io~=1.2.4",
|
|
"croniter~=6.0.0",
|
|
"python-dateutil~=2.9.0",
|
|
"python-docx==1.2.0",
|
|
"python-snappy~=0.7.3",
|
|
"python3-saml==1.16.0",
|
|
"pytz==2023.3",
|
|
"rapidfuzz==3.14.5",
|
|
"redis==5.3.1",
|
|
"requests~=2.33.0",
|
|
"retry==0.9.2",
|
|
"s3fs==2025.9.0",
|
|
"scikit-learn~=1.7.0",
|
|
"scipy~=1.16.3",
|
|
"semantic-version==2.8.5",
|
|
"simple-salesforce~=1.12.9",
|
|
"slack-sdk==3.42.0",
|
|
"snowflake-connector-python==4.4.0",
|
|
"social-auth-app-django~=5.8.0",
|
|
"social-auth-core~=4.8.7",
|
|
"django-scim2==0.23.0",
|
|
"sqlparse==0.5.4",
|
|
"sshtunnel==0.4.0",
|
|
"statshog==1.0.6",
|
|
"stripe==12.2.0",
|
|
"structlog==25.5.0",
|
|
"temporalio==1.33.0",
|
|
"tenacity==9.1.4",
|
|
"tiktoken==0.12.*",
|
|
"tldextract~=5.1",
|
|
"trafilatura==2.0.0",
|
|
"trino==0.338.0",
|
|
"types-cachetools==5.5.0.20240820",
|
|
"css-inline==0.18.0",
|
|
"click~=8.0",
|
|
# Distributed owners.yaml resolver. A runtime dependency, not just tooling: stamphog's digest
|
|
# reads a repo's team -> Slack channel registry through it (products/stamphog/backend/logic/
|
|
# channel_resolution.py). Adds no third-party code — its own deps, pyyaml and click, are here.
|
|
"owners-yaml",
|
|
"personhog-proto",
|
|
"umap-learn~=0.5.0",
|
|
"tbb==2022.3.1; sys_platform == 'linux' and platform_machine == 'x86_64'",
|
|
"whitenoise~=6.10.0",
|
|
"xgboost==3.2.0",
|
|
# CPU inference for the inbox ranking network. Keep in step with the `inbox-ranking` training group.
|
|
"onnxruntime~=1.30.0",
|
|
"xmlsec==1.3.17",
|
|
"zstd==1.5.7.2",
|
|
"zxcvbn==4.5.0",
|
|
"pyyaml~=6.0.2",
|
|
"chdb==4.3.0",
|
|
# chdb allows any chdb-core>=26.7.0, and engines before 26.9.0 accept a TLS certificate issued for another host.
|
|
"chdb-core==26.9.0",
|
|
"elevenlabs~=2.1.0",
|
|
"django-oauth-toolkit~=3.2.0",
|
|
"azure-ai-projects~=1.0.0b11",
|
|
"azure-ai-inference~=1.0.0b9",
|
|
"azure-storage-blob~=12.27.0",
|
|
"opentelemetry-sdk==1.35.0",
|
|
"opentelemetry-instrumentation-aiokafka==0.56b0",
|
|
"opentelemetry-instrumentation-aiohttp-client==0.56b0",
|
|
"opentelemetry-instrumentation-asgi==0.56b0",
|
|
"opentelemetry-instrumentation-celery==0.56b0",
|
|
"opentelemetry-instrumentation-django==0.56b0",
|
|
"opentelemetry-instrumentation-kafka-python==0.56b0",
|
|
"opentelemetry-instrumentation-psycopg==0.56b0",
|
|
"opentelemetry-instrumentation-redis==0.56b0",
|
|
"opentelemetry-instrumentation-requests==0.56b0",
|
|
"opentelemetry-exporter-otlp-proto-grpc==1.35.0",
|
|
"asgiref==3.11.0",
|
|
"user-agents~=2.2.0",
|
|
"django-admin-inline-paginator===0.4.0",
|
|
"fastavro~=1.12.0",
|
|
"pydantic-avro~=0.9.0",
|
|
"playwright~=1.60.0",
|
|
"linkedin-api-client~=0.3.0",
|
|
"glom~=24.11.0",
|
|
"modal[api-proxy-support]~=1.5.4",
|
|
"databricks-sql-connector~=4.3.0",
|
|
"databricks-sdk~=0.118.0",
|
|
"free-email-domains~=1.0.1",
|
|
"disposable-email-domains~=0.0.140",
|
|
"pymediainfo~=7.0.1",
|
|
"cachetools~=5.5.2",
|
|
"gitpython~=3.1.44",
|
|
"duckdb~=1.5.2",
|
|
"bingads~=13.0.26",
|
|
"markdown-to-mrkdwn~=0.2.0",
|
|
"webauthn>=2.1.0,<2.3.0",
|
|
"botocore[crt]~=1.40.49",
|
|
"defusedxml~=0.7.1",
|
|
"confluent-kafka~=2.13",
|
|
"blake3>=1.0.8,<2",
|
|
"pixelhog~=1.3.1",
|
|
"google-re2>=1.1.20251105,<2",
|
|
"scrubadub~=2.0.1",
|
|
"braintrust-core>=0.0.59,<1",
|
|
"wrapt==1.17.3",
|
|
"limits==5.8.0",
|
|
"deltalite==0.1.15",
|
|
"posthog-hogland==0.4.1",
|
|
"requests-oauthlib~=2.0.0",
|
|
"adlfs~=2026.8.0",
|
|
]
|
|
|
|
[dependency-groups]
|
|
dev = [
|
|
{ include-group = "proto-codegen" },
|
|
"aioresponses==0.7.6",
|
|
"autoevals==0.2.0",
|
|
"boto3-stubs[s3]~=1.38.30",
|
|
"browser-cookie3~=0.20",
|
|
"braintrust==0.2.4",
|
|
"braintrust-langchain==0.0.4",
|
|
"dagster-dg-cli~=1.12.11",
|
|
"datamodel-code-generator~=0.36.0",
|
|
"debugpy~=1.8.0",
|
|
"import-linter~=2.11",
|
|
"deepdiff~=8.6.1",
|
|
"django-linear-migrations~=2.19.0",
|
|
"django-stubs~=5.2.0",
|
|
# django-stubs requires this unbounded, so an upgrade run drifts it to a version
|
|
# that models a different Django. Hold it on the line django-stubs is on.
|
|
"django-stubs-ext~=5.2.5",
|
|
"djangorestframework-stubs~=3.16.0",
|
|
"faker==17.5.0",
|
|
"fakeredis[lua]==2.23.3",
|
|
"flameprof~=0.4",
|
|
"hypothesis~=6.151.9",
|
|
"pyinstrument~=5.1",
|
|
# 16.6.1 and later write a retried failure to JUnit as an extra passing <testcase>, which reads as pass-on-retry.
|
|
"pytest-rerunfailures==16.6",
|
|
"time-machine==3.5.1",
|
|
"mypy~=2.1.0",
|
|
"mypy-extensions~=1.1.0",
|
|
"ty==0.0.80",
|
|
# openapi-spec-validator needed for prance as a validation backend
|
|
"openapi-spec-validator==0.7.1",
|
|
"orjson==3.11.9",
|
|
"packaging==25.0",
|
|
"parameterized==0.9.0",
|
|
"prompt-toolkit~=3.0.51",
|
|
"prance==23.6.21.0",
|
|
"pyarrow==23.0.1",
|
|
"pyarrow-stubs==20.0.0.20260625",
|
|
"pytest~=9.1.1",
|
|
"pytest-asyncio~=1.4.0",
|
|
"pytest-aiohttp~=1.1.1",
|
|
"pytest-cov~=7.1.0",
|
|
"pytest-django~=4.14.0",
|
|
"pytest-durations~=1.9.0",
|
|
"pytest-env~=1.7.1",
|
|
"pytest-icdiff~=0.9",
|
|
"pytest-mock~=3.15.1",
|
|
"pytest-split~=0.11.0", # EXPERIMENT: git-pinned below to the optimal_chunks fork
|
|
"pytest-timeout~=2.4.0",
|
|
"pytest-xdist~=3.8.0",
|
|
"python-dateutil~=2.9.0",
|
|
"requests-mock~=1.12.1",
|
|
"responses==0.23.1",
|
|
"stpyv8==13.1.201.22; sys_platform != 'linux' or (platform_machine != 'aarch64' and platform_machine != 'arm64')",
|
|
"syrupy~=6.1.1",
|
|
"tach~=0.34",
|
|
"types-aioboto3[s3]~=14.3.0",
|
|
"types-aiobotocore~=2.22.0",
|
|
"types-boto3[essential,ses,sesv2]==1.40.49",
|
|
"types-croniter~=6.0.0",
|
|
"types-markdown==3.3.9",
|
|
"types-paramiko==3.4.0.20240423",
|
|
"types-pymysql==1.1.0.20240524",
|
|
"types-python-dateutil~=2.9.0",
|
|
"types-pytz==2023.3",
|
|
"types-pyyaml==6.0.1",
|
|
"types-redis==4.6.0.20241004",
|
|
# >= 2.31.0.7 versions of types-requests require urllib>=2, which is incompatible with our dependencies
|
|
"types-requests==2.31.0.6",
|
|
"types-retry==0.9.9.4",
|
|
"types-tzlocal~=5.1.0.1",
|
|
"pywatchman~=4.0",
|
|
"watchdog==5.0.3",
|
|
"testcontainers[mysql]~=4.14",
|
|
# Developer CLI
|
|
"hogli",
|
|
"libcst>=1.8.6,<2.0.0",
|
|
]
|
|
sentiment = [
|
|
"optimum[onnxruntime]~=1.25.0",
|
|
"torch~=2.9.0",
|
|
]
|
|
# The inbox ranking multi-head network (products/signals/dags/inbox_ranking/multihead/). Only
|
|
# Dockerfile.dagster-ml installs it. torch comes from the CPU wheel index, see [tool.uv.sources].
|
|
inbox-ranking = [
|
|
"onnx~=1.21.0",
|
|
"onnxruntime~=1.30.0",
|
|
"torch~=2.9.0",
|
|
]
|
|
hogql-parser-parity = [
|
|
"shrinkray==26.4.14.0",
|
|
]
|
|
proto-codegen = [
|
|
"grpcio-tools~=1.75.1",
|
|
"ruff~=0.15.20",
|
|
]
|
|
|
|
[tool.uv]
|
|
required-version = ">=0.13.4"
|
|
add-bounds = "major"
|
|
exclude-newer = "7 days"
|
|
exclude-newer-package = { brotli = false, certifi = false, hogql-parser = false, hogql-parser-rs = false, deltalite = false, posthoganalytics = false, pixelhog = false, django = false, modal = false, posthog-hogland = false, google-ads = false, grpcio = false }
|
|
# posthoganalytics 8 removes legacy capture. The SDK release bot rewrites the
|
|
# dependency spec on every release, so this constraint is what stops a new major from
|
|
# landing by accident. Lift it in the migration PR that moves the call sites.
|
|
constraint-dependencies = ["posthoganalytics<8"]
|
|
# sentiment takes torch from PyPI with the CUDA libraries, inbox-ranking takes the CPU-only build.
|
|
conflicts = [[{ group = "sentiment" }, { group = "inbox-ranking" }]]
|
|
|
|
[[tool.uv.index]]
|
|
name = "pytorch-cpu"
|
|
url = "https://download.pytorch.org/whl/cpu"
|
|
explicit = true
|
|
|
|
[tool.uv.workspace]
|
|
members = ["tools/hogli", "packages/owners-yaml", "packages/personhog-proto"]
|
|
|
|
[tool.uv.sources]
|
|
hogli = { workspace = true }
|
|
owners-yaml = { workspace = true }
|
|
personhog-proto = { workspace = true }
|
|
infi-clickhouse-orm = { git = "https://github.com/PostHog/infi.clickhouse_orm", rev = "fa20b806e32cf748dfee3545d8926b9c36aff789" }
|
|
# pytest-split: pinned to PostHog's fork (immutable tag) for the order-preserving
|
|
# optimal_chunks split. Fork: PostHog/pytest-split (of jerry-git/pytest-split).
|
|
# To pull upstream fixes: rebase the fork's feat branch onto upstream, cut the next
|
|
# v0.11.0-posthog.N tag, and bump this pin.
|
|
pytest-split = { git = "https://github.com/PostHog/pytest-split", tag = "v0.11.0-posthog.8" }
|
|
torch = [{ index = "pytorch-cpu", group = "inbox-ranking" }]
|
|
|
|
# xgboost's Linux wheel declares nvidia-nccl-cu12 (322 MB) for multi-GPU training. We use xgboost on
|
|
# CPU, so drop it to keep the wheel out of the image.
|
|
[[tool.uv.dependency-metadata]]
|
|
name = "xgboost"
|
|
version = "3.2.0"
|
|
requires-dist = ["numpy", "scipy"]
|
|
|
|
|
|
[tool.isort]
|
|
profile = "black"
|
|
|
|
[tool.ruff]
|
|
line-length = 120
|
|
# Pinned apart from requires-python. Raising it to py314 makes ruff format drop the parentheses
|
|
# in `except (A, B):` (PEP 758) across the repo, and makes UP037 flag every quoted annotation.
|
|
target-version = "py313"
|
|
force-exclude = true
|
|
exclude = [
|
|
".git",
|
|
"./.semgrep",
|
|
"./nodejs/node_modules/",
|
|
"./plugins/node_modules/",
|
|
"./env",
|
|
"./posthog/hogql/grammar",
|
|
"products/*/skills/*/scripts",
|
|
# imported desktop app; Biome-linted by its own desktop-* CI
|
|
"products/desktop",
|
|
]
|
|
|
|
[tool.ruff.lint]
|
|
ignore = [
|
|
"B017", # ~38: assertRaises(Exception) too broad
|
|
"B904", # ~488: raise without from in except
|
|
"B905", # ~153: zip() without strict=
|
|
"C901", # ~527: function too complex
|
|
"E501", # ~4697: line too long
|
|
"E722", # ~62: bare except
|
|
"E731", # ~185: lambda assignment
|
|
"F541", # ~1139: f-string missing placeholders
|
|
"UP007", # ~377: Union[X,Y] → X | Y (auto-fixable)
|
|
"UP032", # ~175: use f-string (auto-fixable)
|
|
"UP045", # ~3064: Optional[X] → X | None (auto-fixable)
|
|
"UP046", # ~39: generic class → PEP 695 (unsafe)
|
|
"UP047", # ~37: generic func → PEP 695 (unsafe)
|
|
"UP042", # ~28: str+Enum → StrEnum (unsafe: changes str()/format() output)
|
|
]
|
|
select = [
|
|
"B",
|
|
"C4",
|
|
"C9",
|
|
"DJ012",
|
|
"E",
|
|
"F",
|
|
"I",
|
|
"PLC0415",
|
|
"RUF005",
|
|
"RUF013",
|
|
"RUF015",
|
|
"RUF019",
|
|
"T100",
|
|
"T2",
|
|
"TID253",
|
|
"TRY201",
|
|
"TRY400",
|
|
"UP",
|
|
"W",
|
|
]
|
|
|
|
[tool.ruff.lint.per-file-ignores]
|
|
# protoc's _pb2.py files import dependencies for the side effect of loading their descriptors
|
|
# into the pool before AddSerializedFile(). ruff must not strip or move those imports.
|
|
"packages/personhog-proto/**/*_pb2.py" = ["F401", "E402"]
|
|
# deltalite's test suite bootstraps its harness/planner via sys.path (the package is a
|
|
# maturin-built extension living under rust/, not part of the Django app), so imports
|
|
# are necessarily lazy / post-path-insert (PLC0415, E402); the planner's small CLI
|
|
# prints its report (T201).
|
|
"rust/deltalite/python/**" = ["PLC0415", "E402", "T201"]
|
|
# Django settings aggregator: star imports are how settings modules merge into one namespace.
|
|
# Not portable to explicit re-exports without losing the settings-loading semantics.
|
|
"posthog/settings/**" = ["F403", "F405"]
|
|
|
|
# PLC0415 exemptions. products/ and services/llm-gateway/ have their own ruff configs.
|
|
# Run `ruff check <path> --select PLC0415 --statistics` before removing an entry.
|
|
".github/**" = ["PLC0415"]
|
|
"bin/**" = ["PLC0415"]
|
|
"common/**" = ["PLC0415"]
|
|
"ee/**" = ["PLC0415"]
|
|
"manage.py" = ["PLC0415"]
|
|
"posthog/**" = ["PLC0415"]
|
|
"scripts/**" = ["PLC0415"]
|
|
"services/stripe-mock/**" = ["PLC0415"]
|
|
"tools/**" = ["PLC0415"]
|
|
|
|
[tool.ruff.lint.flake8-tidy-imports]
|
|
# Heavy SDKs that noticeably slow startup. They must be imported lazily (inside the
|
|
# function that uses them), not at module top, so they stay off the Django app-load path.
|
|
# Only modules with a single, already-lazy call site are listed — vendor source/destination
|
|
# leaf modules legitimately import their own SDK at module level and are loaded on demand.
|
|
banned-module-level-imports = ["elevenlabs", "matplotlib", "trafilatura"]
|
|
|
|
[tool.ruff.lint.mccabe]
|
|
max-complexity = 10
|
|
|
|
[tool.ruff.lint.isort]
|
|
combine-as-imports = true
|
|
force-wrap-aliases = true
|
|
length-sort-straight = true
|
|
split-on-trailing-comma = false
|
|
section-order = [
|
|
"future",
|
|
"standard-library",
|
|
"testing",
|
|
"django",
|
|
"third-party",
|
|
"posthog.schema",
|
|
"posthog.hogql",
|
|
"posthog",
|
|
"products",
|
|
"first-party",
|
|
"local-folder",
|
|
]
|
|
|
|
[tool.ruff.lint.isort.sections]
|
|
"django" = ["django"] # Group all Django imports into a separate section.
|
|
"testing" = [ # Group all testing imports into a separate section.
|
|
"pytest",
|
|
"time_machine",
|
|
"unittest",
|
|
"posthog.test.base",
|
|
]
|
|
# Separate groups for our own stuff
|
|
"posthog.schema" = ["posthog.schema"]
|
|
"posthog.hogql" = ["posthog.hogql"]
|
|
"posthog" = ["posthog"]
|
|
"products" = ["products"]
|
|
|
|
[tool.djlint]
|
|
profile = "django"
|
|
extension = "html"
|
|
indent = 4
|
|
# Formatting covers every Django template; linting is scoped to the email ones, so these
|
|
# exclusions are read against those. H021 inline styles and H023 entity references are how
|
|
# HTML email has to be written, since mail clients strip stylesheets and render bare
|
|
# characters inconsistently. H016 title and H030 meta description are page rules that mean
|
|
# nothing in an email.
|
|
ignore = "H021,H023,H016,H030"
|
|
|
|
[tool.dagster]
|
|
python_version = "3.14"
|
|
|
|
[tool.mypy]
|
|
python_version = "3.14"
|
|
plugins = [
|
|
"mypy_django_plugin.main",
|
|
"mypy_drf_plugin.main",
|
|
"pydantic.mypy",
|
|
]
|
|
strict_optional = true
|
|
no_implicit_optional = true
|
|
warn_unused_ignores = true
|
|
check_untyped_defs = true
|
|
warn_unreachable = true
|
|
strict_equality = true
|
|
ignore_missing_imports = true
|
|
exclude_gitignore = true
|
|
# conftest.py: the root pytest bootstrap collides with services/stripe-mock/tests/conftest.py
|
|
# (both resolve to top-level module "conftest")
|
|
# rust/deltalite/python: pytest suite for the maturin-built extension; nothing Django
|
|
# imports it and it has its own CI (ci-deltalite-python.yml).
|
|
# packages/owners-yaml: a standalone distribution that must build and run with no Django. This config
|
|
# loads the Django and DRF plugins and the app's settings, so it is the wrong checker for it. ty
|
|
# covers it repo-wide instead, and ci-python builds and exercises the wheel in an empty venv.
|
|
exclude = ["^(bin/.*|tools/.*|packages/owners-yaml/.*|packages/personhog-proto/.*|rust/deltalite/python/.*|products/desktop/.*|products/stamphog/packages/.*|products/[^/]+/skills/[^/]+/scripts/.*|gunicorn\\.config\\.py|manage\\.py|conftest\\.py)$"]
|
|
|
|
[[tool.mypy.overrides]]
|
|
module = "bin.*"
|
|
ignore_errors = true
|
|
|
|
[[tool.mypy.overrides]]
|
|
module = "posthog.hogql.grammar.*"
|
|
ignore_errors = true
|
|
|
|
# Profiling code from the nextgensquash tooling — pyinstrument/py-spy
|
|
# wrappers do dynamic method swaps that don't fit the static stubs.
|
|
[[tool.mypy.overrides]]
|
|
module = "posthog.management.migration_profiling.*"
|
|
ignore_errors = true
|
|
|
|
[[tool.mypy.overrides]]
|
|
module = "posthog.tasks.*"
|
|
disallow_untyped_defs = true
|
|
check_untyped_defs = true
|
|
|
|
[tool.django-stubs]
|
|
django_settings_module = "posthog.settings"
|
|
|
|
[tool.pyright]
|
|
venvPath = ".flox/cache"
|
|
venv = "venv"
|
|
pythonVersion = "3.14"
|
|
|
|
[tool.ty.environment]
|
|
python-version = "3.14"
|
|
|
|
[tool.ty.src]
|
|
exclude = [
|
|
"bin/**",
|
|
"manage.py",
|
|
"posthog/hogql/grammar/**",
|
|
"packages/personhog-proto/**",
|
|
"posthog/schema.py",
|
|
"posthog/schema_enums.py",
|
|
"posthog/temporal/proxy_service/proto/**",
|
|
# PEP 723 scripts with their own deps; mypy excludes these dirs too
|
|
"products/*/skills/*/scripts/**",
|
|
"products/stamphog/packages/**",
|
|
"tools/**",
|
|
"products/warehouse_sources/backend/temporal/data_imports/sources/generated_configs/**",
|
|
"rust/deltalite/python/**",
|
|
]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# import-linter: enforce internal product architecture
|
|
# ---------------------------------------------------------------------------
|
|
# Ensures products follow the facade pattern internally:
|
|
# presentation → facade → logic/models (not shortcuts).
|
|
# Complements tach (inter-module boundaries) with intra-product enforcement.
|
|
|
|
[tool.importlinter]
|
|
root_packages = ["products", "owners_yaml"]
|
|
include_external_packages = true
|
|
|
|
# A Python distribution under packages/ is a leaf: it must install and run outside the
|
|
# monorepo venv, so it can import nothing from the app. tach resolves a uv distribution as
|
|
# third-party and cannot police that boundary. import-linter resolves by installed import
|
|
# name, so it can.
|
|
[[tool.importlinter.contracts]]
|
|
name = "packages/owners-yaml is a leaf"
|
|
type = "forbidden"
|
|
source_modules = ["owners_yaml"]
|
|
forbidden_modules = ["posthog", "ee", "products", "common", "django", "rest_framework"]
|
|
|
|
# Presentation must only reach product internals through the facade.
|
|
# Forbid all backend imports, then allowlist facade + self-imports.
|
|
# This catches any new internal modules (cache.py, helpers.py, etc.)
|
|
# automatically — no blocklist to maintain.
|
|
[[tool.importlinter.contracts]]
|
|
name = "presentation must use facade"
|
|
type = "forbidden"
|
|
source_modules = ["products.*.backend.presentation"]
|
|
forbidden_modules = [
|
|
"products.*.backend",
|
|
]
|
|
allow_indirect_imports = true
|
|
ignore_imports = [
|
|
# Allowed: presentation can import facade and itself
|
|
"products.**.backend.presentation.** -> products.**.backend.facade.**",
|
|
"products.**.backend.presentation.** -> products.**.backend.presentation.**",
|
|
# TODO: cdp presentation wave — the helpers, serializers and icon service other products' views
|
|
# reuse still live in cdp internals, and products/cdp/backend/presentation/ re-exports them.
|
|
# Move each one into presentation and delete its entry.
|
|
"products.cdp.backend.presentation.hog_functions -> products.cdp.backend.filters",
|
|
"products.cdp.backend.presentation.hog_functions -> products.cdp.backend.validation",
|
|
"products.cdp.backend.presentation.serializers -> products.cdp.backend.validation",
|
|
"products.cdp.backend.presentation.hog_functions -> products.cdp.backend.flag_gated_templates",
|
|
"products.cdp.backend.presentation.icons -> products.cdp.backend.services.icons",
|
|
# TODO: existing violations — fix by routing through facade
|
|
"products.alerts.backend.presentation.views.alert -> products.alerts.backend.evaluation.contract",
|
|
"products.alerts.backend.presentation.views.alert -> products.alerts.backend.evaluation.detector",
|
|
"products.alerts.backend.presentation.views.alert -> products.alerts.backend.evaluation.validation",
|
|
"products.alerts.backend.presentation.views.alert -> products.alerts.backend.insight_alert_state_machine",
|
|
"products.alerts.backend.presentation.views.alert -> products.alerts.backend.models.alert",
|
|
"products.alerts.backend.presentation.views.insight_alerts -> products.alerts.backend.models.alert",
|
|
"products.mcp_analytics.backend.presentation.views -> products.mcp_analytics.backend.logic",
|
|
"products.mcp_analytics.backend.presentation.views -> products.mcp_analytics.backend.models",
|
|
"products.mcp_analytics.backend.presentation.serializers -> products.mcp_analytics.backend.models",
|
|
"products.mcp_store.backend.presentation.agent_views -> products.mcp_store.backend.agents",
|
|
"products.mcp_store.backend.presentation.agent_views -> products.mcp_store.backend.gateway",
|
|
"products.mcp_store.backend.presentation.agent_views -> products.mcp_store.backend.models",
|
|
"products.mcp_store.backend.presentation.agent_views -> products.mcp_store.backend.policy",
|
|
"products.mcp_store.backend.presentation.agent_views -> products.mcp_store.backend.proxy",
|
|
"products.mcp_store.backend.presentation.gateway_views -> products.mcp_store.backend.agents",
|
|
"products.mcp_store.backend.presentation.gateway_views -> products.mcp_store.backend.gateway",
|
|
"products.mcp_store.backend.presentation.gateway_views -> products.mcp_store.backend.models",
|
|
"products.mcp_store.backend.presentation.gateway_views -> products.mcp_store.backend.policy",
|
|
"products.mcp_store.backend.presentation.views -> products.mcp_store.backend.agents",
|
|
"products.mcp_store.backend.presentation.views -> products.mcp_store.backend.gateway",
|
|
"products.mcp_store.backend.presentation.views -> products.mcp_store.backend.models",
|
|
"products.mcp_store.backend.presentation.views -> products.mcp_store.backend.oauth",
|
|
"products.mcp_store.backend.presentation.views -> products.mcp_store.backend.policy",
|
|
"products.mcp_store.backend.presentation.views -> products.mcp_store.backend.proxy",
|
|
"products.mcp_store.backend.presentation.views -> products.mcp_store.backend.tasks",
|
|
"products.mcp_store.backend.presentation.views -> products.mcp_store.backend.tools",
|
|
"products.notifications.backend.presentation.views -> products.notifications.backend.cache",
|
|
"products.notifications.backend.presentation.views -> products.notifications.backend.models",
|
|
"products.tracing.backend.presentation.views -> products.tracing.backend.logic",
|
|
"products.tracing.backend.presentation.views -> products.tracing.backend.sparkline_query_runner",
|
|
"products.tracing.backend.presentation.views -> products.tracing.backend.has_spans_query_runner",
|
|
# Saved-views ModelViewSet needs the model class for its serializer; same accepted pattern as
|
|
# logs' views_api. TODO: route through facade with the broader presentation cleanup.
|
|
"products.tracing.backend.presentation.views_api -> products.tracing.backend.models",
|
|
# logs presentation isolation (temporary): the moved viewsets still reach in-product
|
|
# internals directly. Removed by the follow-up that thins each viewset to go through
|
|
# the facade.
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.column_expressions",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.count_query_runner",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.count_ranges_query_runner",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.group_by_query_runner",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.has_logs_query_runner",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.impact_query_runner",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.log_attributes_query_runner",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.log_facet_values_query_runner",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.log_values_query_runner",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.logs_query_runner",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.pattern_diff",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.patterns_query_runner",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.services_query_runner",
|
|
"products.logs.backend.presentation.views.api -> products.logs.backend.sparkline_query_runner",
|
|
"products.logs.backend.presentation.views.alerts_api -> products.logs.backend.alert_check_query",
|
|
"products.logs.backend.presentation.views.alerts_api -> products.logs.backend.alert_destinations",
|
|
"products.logs.backend.presentation.views.alerts_api -> products.logs.backend.alert_state_machine",
|
|
"products.logs.backend.presentation.views.alerts_api -> products.logs.backend.models",
|
|
"products.logs.backend.presentation.views.metric_rules_api -> products.logs.backend.models",
|
|
"products.logs.backend.presentation.views.retention_api -> products.logs.backend.models",
|
|
"products.logs.backend.presentation.views.sampling_api -> products.logs.backend.models",
|
|
"products.logs.backend.presentation.views.views_api -> products.logs.backend.models",
|
|
"products.logs.backend.presentation.views.anomalies_api -> products.logs.backend.anomaly_scan",
|
|
"products.logs.backend.presentation.views.anomalies_api -> products.logs.backend.series_bands",
|
|
# logs presentation isolation (temporary): cross-product imports the moved viewsets
|
|
# still make directly. These resolve once cdp/exports expose the needed facades, so
|
|
# they outlive the in-product cleanup above.
|
|
"products.logs.backend.presentation.views.api -> products.exports.backend.models.exported_asset",
|
|
# TODO: notebooks presentation wave — the moved NotebookViewSet still reaches in-product
|
|
# internals (collab, kernel runtime, query validation, models) and the tasks sandbox
|
|
# directly. Thin it to parse -> facade -> serialize and delete each entry.
|
|
# backend:contract-check stays off until this list is empty.
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.activity_logging",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.analytics",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.collab",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.collab_stream",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.kernel_runtime",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.markdown_collab",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.models",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.presence",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.python_analysis",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.query_validation",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.sql_v2",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.sql_v2_direct",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.sql_v2_runs",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.sql_v2_serializers",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.sql_v2_state",
|
|
"products.notebooks.backend.presentation.views.notebook -> products.notebooks.backend.sql_v2_variables",
|
|
# TODO: experiments presentation wave — these were invisible to import-linter until
|
|
# backend/presentation/ got its package marker, so they are newly surfaced debt, not new
|
|
# coupling. experiments is not isolated, so nothing was being skipped on their account.
|
|
"products.experiments.backend.presentation.serializers -> products.ai_observability.backend.models.llm_prompt",
|
|
"products.experiments.backend.presentation.serializers -> products.experiments.backend.experiment_service",
|
|
"products.experiments.backend.presentation.serializers -> products.experiments.backend.facade",
|
|
"products.experiments.backend.presentation.serializers -> products.experiments.backend.hogql_queries.exposure_query_logic",
|
|
"products.experiments.backend.presentation.serializers -> products.experiments.backend.llm_metric_templates",
|
|
"products.experiments.backend.presentation.serializers -> products.experiments.backend.metric_events",
|
|
"products.experiments.backend.presentation.serializers -> products.experiments.backend.metric_utils",
|
|
"products.experiments.backend.presentation.serializers -> products.experiments.backend.models.experiment",
|
|
"products.experiments.backend.presentation.serializers -> products.experiments.backend.running_time_calculator",
|
|
"products.experiments.backend.presentation.serializers -> products.experiments.backend.session_buckets",
|
|
"products.experiments.backend.presentation.serializers -> products.experiments.backend.session_context",
|
|
"products.experiments.backend.presentation.serializers -> products.experiments.backend.session_event_deltas",
|
|
"products.experiments.backend.presentation.serializers -> products.experiments.backend.setup_context",
|
|
"products.experiments.backend.presentation.serializers -> products.feature_flags.backend.api.feature_flag",
|
|
"products.experiments.backend.presentation.serializers -> products.feature_flags.backend.models.feature_flag",
|
|
"products.experiments.backend.presentation.views -> products.approvals.backend.mixins",
|
|
"products.experiments.backend.presentation.views -> products.experiments.backend.experiment_service",
|
|
"products.experiments.backend.presentation.views -> products.experiments.backend.llm_metric_templates",
|
|
"products.experiments.backend.presentation.views -> products.experiments.backend.models.experiment",
|
|
"products.experiments.backend.presentation.views -> products.experiments.backend.recalculation",
|
|
"products.experiments.backend.presentation.views -> products.experiments.backend.running_time_calculator",
|
|
"products.experiments.backend.presentation.views -> products.experiments.backend.session_buckets",
|
|
"products.experiments.backend.presentation.views -> products.experiments.backend.session_context",
|
|
"products.experiments.backend.presentation.views -> products.experiments.backend.session_event_deltas",
|
|
"products.experiments.backend.presentation.views -> products.experiments.backend.setup_context",
|
|
"products.experiments.backend.presentation.views -> products.feature_flags.backend.models.evaluation_context",
|
|
"products.experiments.backend.presentation.views -> products.feature_flags.backend.models.feature_flag",
|
|
# TODO: data_warehouse presentation wave — the structural move landed the viewsets under
|
|
# presentation/views/ but they still reach in-product internals and sibling products
|
|
# (warehouse_sources, data_modeling, data_tools, cdp, revenue_analytics, batch_exports)
|
|
# directly. Thin each viewset to parse -> facade -> serialize and delete each entry.
|
|
# backend:contract-check stays off until this list is empty.
|
|
# TODO: access-control presentation wave — the consolidated RBAC viewsets carry their ORM
|
|
# access with them. Thin them to parse -> facade -> serialize and delete each entry.
|
|
"products.access_control.backend.presentation.access_control -> products.access_control.backend.models.access_control",
|
|
"products.access_control.backend.presentation.access_control_settings -> products.access_control.backend.models.access_control",
|
|
"products.access_control.backend.presentation.access_control_settings -> products.access_control.backend.models.role",
|
|
"products.access_control.backend.presentation.role -> products.access_control.backend.models.role",
|
|
# TODO: batch_exports presentation wave — the moved viewsets, serializers and destination
|
|
# test helpers still reach in-product internals directly. Thin each one to parse -> facade
|
|
# -> serialize and delete each entry. backend:contract-check stays off until this list is
|
|
# empty. Only delete entries here: a new import from these views goes through the facade.
|
|
"products.batch_exports.backend.presentation.views.batch_export.destinations -> products.batch_exports.backend.models.batch_export",
|
|
"products.batch_exports.backend.presentation.views.batch_export.exports -> products.batch_exports.backend.hogql_source",
|
|
"products.batch_exports.backend.presentation.views.batch_export.exports -> products.batch_exports.backend.models.batch_export",
|
|
"products.batch_exports.backend.presentation.views.batch_export.exports -> products.batch_exports.backend.service",
|
|
]
|
|
|
|
# routes.py is the one module core reads to assemble a product's API router, so it is the
|
|
# only way a view becomes reachable over HTTP. Restricting what it may import gives the chain
|
|
# core -> routes -> presentation -> facade as three import edges: a ViewSet outside
|
|
# presentation/ cannot be routed, and the contract above already keeps presentation/ from
|
|
# reaching past the facade. Grandfathered edges below are products whose views still live in
|
|
# backend/api/; `hogli product:isolate:move` relocates them and the line comes out.
|
|
[[tool.importlinter.contracts]]
|
|
name = "routes must only import presentation"
|
|
type = "forbidden"
|
|
source_modules = ["products.*.backend.routes"]
|
|
forbidden_modules = [
|
|
"products.*.backend",
|
|
]
|
|
allow_indirect_imports = true
|
|
ignore_imports = [
|
|
"products.**.backend.routes -> products.**.backend.presentation.**",
|
|
# TODO: views still registered from backend/api/ — move them into presentation/ and drop the line
|
|
"products.actions.backend.routes -> products.actions.backend.api.action",
|
|
"products.ai_observability.backend.routes -> products.ai_observability.backend.api",
|
|
"products.annotations.backend.routes -> products.annotations.backend.api.annotation",
|
|
"products.approvals.backend.routes -> products.approvals.backend.api",
|
|
"products.business_knowledge.backend.routes -> products.business_knowledge.backend.api",
|
|
"products.cdp.backend.routes -> products.cdp.backend.api.hog_function",
|
|
"products.cdp.backend.routes -> products.cdp.backend.api.hog_function_template",
|
|
"products.cdp.backend.routes -> products.cdp.backend.api.plugin",
|
|
"products.cdp.backend.routes -> products.cdp.backend.api.plugin_log_entry",
|
|
"products.cohorts.backend.routes -> products.cohorts.backend.api.staff_tools",
|
|
"products.conversations.backend.routes -> products.conversations.backend.api",
|
|
"products.dashboards.backend.routes -> products.dashboards.backend.api.dashboard",
|
|
"products.dashboards.backend.routes -> products.dashboards.backend.api.dashboard_templates",
|
|
"products.early_access_features.backend.routes -> products.early_access_features.backend.api",
|
|
"products.exports.backend.routes -> products.exports.backend.api.exports",
|
|
"products.feature_flags.backend.routes -> products.feature_flags.backend.api.feature_flag",
|
|
"products.feature_flags.backend.routes -> products.feature_flags.backend.api.flag_value",
|
|
"products.feature_flags.backend.routes -> products.feature_flags.backend.api.organization_feature_flag",
|
|
"products.feature_flags.backend.routes -> products.feature_flags.backend.api.scheduled_change",
|
|
"products.feature_flags.backend.routes -> products.feature_flags.backend.api.staff_cache",
|
|
"products.feature_flags.backend.routes -> products.feature_flags.backend.api.staff_team_config",
|
|
"products.feature_flags.backend.routes -> products.feature_flags.backend.api.staff_teams",
|
|
"products.field_notes.backend.routes -> products.field_notes.backend.api",
|
|
"products.growth.backend.routes -> products.growth.backend.api.ai_enrichment",
|
|
"products.growth.backend.routes -> products.growth.backend.api.identity_matching",
|
|
"products.growth.backend.routes -> products.growth.backend.api.product_push",
|
|
"products.managed_migrations.backend.routes -> products.managed_migrations.backend.api.batch_imports",
|
|
"products.managed_migrations.backend.routes -> products.managed_migrations.backend.api.support_batch_imports",
|
|
"products.marketing_analytics.backend.routes -> products.marketing_analytics.backend.api",
|
|
"products.posthog_ai.backend.routes -> products.posthog_ai.backend.api",
|
|
"products.product_tours.backend.routes -> products.product_tours.backend.api",
|
|
"products.pulse.backend.routes -> products.pulse.backend.api.brief",
|
|
"products.reminders.backend.routes -> products.reminders.backend.api.reminder",
|
|
"products.replay_vision.backend.routes -> products.replay_vision.backend.api",
|
|
"products.revenue_analytics.backend.routes -> products.revenue_analytics.backend.api",
|
|
"products.signals.backend.routes -> products.signals.backend.views",
|
|
"products.signals.backend.routes -> products.signals.backend.scout_chat",
|
|
"products.signals.backend.routes -> products.signals.backend.scout_harness.views",
|
|
"products.signals.backend.routes -> products.signals.backend.scout_suggestions_api",
|
|
"products.skills.backend.routes -> products.skills.backend.api.community_skills",
|
|
"products.skills.backend.routes -> products.skills.backend.api.marketplace_views",
|
|
"products.skills.backend.routes -> products.skills.backend.api.skills",
|
|
"products.surveys.backend.routes -> products.surveys.backend.api.survey",
|
|
"products.web_analytics.backend.routes -> products.web_analytics.backend.api",
|
|
"products.web_analytics.backend.routes -> products.web_analytics.backend.api.custom_bot_rules",
|
|
"products.web_analytics.backend.routes -> products.web_analytics.backend.api.heatmaps_api",
|
|
"products.web_analytics.backend.routes -> products.web_analytics.backend.api.web_analytics_achievements",
|
|
"products.web_analytics.backend.routes -> products.web_analytics.backend.api.web_analytics_filter_preset",
|
|
"products.web_analytics.backend.routes -> products.web_analytics.backend.api.web_analytics_path_cleaning_suggestions",
|
|
]
|
|
|
|
# webhook_consumers.py is the one module core reads (through posthog.ingress.dispatch.loading)
|
|
# to register a product's inbound webhook consumers. Restricting what it may import gives the
|
|
# chain core -> webhook_consumers -> facade as two import edges, so a handler cannot reach
|
|
# product internals.
|
|
[[tool.importlinter.contracts]]
|
|
name = "webhook consumers must only import facade"
|
|
type = "forbidden"
|
|
source_modules = ["products.*.backend.webhook_consumers"]
|
|
forbidden_modules = [
|
|
"products.*.backend",
|
|
]
|
|
allow_indirect_imports = true
|
|
ignore_imports = [
|
|
"products.**.backend.webhook_consumers -> products.**.backend.facade.**",
|
|
]
|
|
|
|
# The facade is the layer below presentation. A facade that imports its product's presentation
|
|
# inverts the layering (and drags DRF onto every consumer's import path); a facade that imports
|
|
# DRF is presentation in disguise. Only direct imports are checked (allow_indirect_imports is
|
|
# forced — without it import-linter rejects the wildcard modules as having shared descendants),
|
|
# so DRF reached through an internal module stays green: this enforces the layering norm, not a
|
|
# transitive guarantee. See products/architecture.md § Wiring couplings.
|
|
[[tool.importlinter.contracts]]
|
|
name = "facade must not import presentation or DRF"
|
|
type = "forbidden"
|
|
source_modules = ["products.*.backend.facade"]
|
|
forbidden_modules = [
|
|
"products.*.backend.presentation",
|
|
"rest_framework",
|
|
]
|
|
allow_indirect_imports = true
|
|
ignore_imports = [
|
|
# TODO: existing violations — move the presentation-shaped code out of the facade, or the
|
|
# facade-shaped code out of presentation, and drop the line
|
|
"products.logs.backend.facade.retention_views -> products.logs.backend.presentation.views.retention_api",
|
|
"products.managed_warehouse.backend.facade.api -> products.managed_warehouse.backend.presentation.views",
|
|
"products.tasks.backend.facade.api -> products.tasks.backend.presentation.serializers",
|
|
"products.billing_alerts.backend.facade.api -> rest_framework",
|
|
"products.dashboards.backend.facade.api -> rest_framework",
|
|
"products.experiments.backend.facade.api -> rest_framework",
|
|
"products.feature_flags.backend.facade.api -> rest_framework",
|
|
"products.tasks.backend.facade.api -> rest_framework",
|
|
"products.tasks.backend.facade.client_provenance -> rest_framework",
|
|
]
|
|
|
|
# Contract files hold the data types other products read, so they stay free of Django, directly
|
|
# and through other product modules. Define a choices enum with posthog.enums.LabeledStrEnum or
|
|
# LabeledIntEnum instead of models.TextChoices. import-linter treats posthog as an external
|
|
# package, so Django reached through posthog.* is not visible here. See products/architecture.md
|
|
# § Contracts.
|
|
[[tool.importlinter.contracts]]
|
|
name = "facade contracts and enums must not import Django (use posthog.enums for choices)"
|
|
type = "forbidden"
|
|
source_modules = [
|
|
"products.*.backend.facade.contracts",
|
|
"products.*.backend.facade.enums",
|
|
"products.*.backend.facade.types",
|
|
]
|
|
forbidden_modules = ["django"]
|
|
allow_indirect_imports = false
|
|
|
|
[tool.ty.rules]
|
|
# Import resolution - ty can't always find all modules/stubs
|
|
unresolved-import = "ignore"
|
|
|
|
# Django-related rules - ty doesn't support Django's metaprogramming yet
|
|
# See: https://github.com/astral-sh/ty/issues/291
|
|
unresolved-attribute = "ignore" # Django model fields, relationships, auto-generated attributes
|
|
invalid-argument-type = "ignore" # Django querysets, managers, form fields
|
|
unknown-argument = "ignore" # Django kwargs, queryset filters, manager methods
|
|
invalid-base = "ignore" # Django model metaclasses (e.g., models.Model)
|
|
missing-argument = "ignore" # Django auto-generated __init__, managers
|
|
too-many-positional-arguments = "ignore" # Django metaprogramming signatures
|
|
|
|
# Dual mypy+ty — mypy still runs, so its artifacts are legitimate
|
|
redundant-cast = "ignore" # casts needed by mypy that ty resolves
|
|
unused-type-ignore-comment = "ignore" # type: ignore comments required by mypy
|
|
unused-ignore-comment = "error" # default is warn, which does not fail CI; error keeps stale ty: ignore directives out
|
|
|
|
# Flags guards on stub and TypedDict types that runtime data does not honor
|
|
# (billing JSON, posthoganalytics.api_key, untyped access-control callers)
|
|
redundant-condition = "ignore"
|
|
|
|
# Django lazy imports and settings indirection
|
|
possibly-missing-submodule = "ignore"
|
|
|
|
# DRF serializer pattern — validate/update/create overrides are idiomatic
|
|
invalid-method-override = "ignore"
|
|
invalid-attribute-override = "ignore" # Django manager/field overrides (e.g. `objects`)
|
|
|
|
# Conditional base classes (e.g. EnterpriseEventQuery | EventQuery pattern)
|
|
unsupported-base = "ignore"
|
|
|
|
# Protobuf generated code + Django lazy references
|
|
unresolved-reference = "ignore"
|
|
|
|
# Third-party library overload stubs (Anthropic SDK, Django, re, etc.)
|
|
no-matching-overload = "ignore"
|
|
|
|
# Framework metaclass magic — ty can't resolve dynamic callables
|
|
call-top-callable = "ignore"
|
|
call-non-callable = "ignore"
|
|
|
|
# Django querysets typed as `object` — ty can't narrow through managers
|
|
not-iterable = "ignore"
|
|
|
|
# `in` operator on Django/framework `object` types
|
|
unsupported-operator = "ignore"
|
|
|
|
# Subscript on `object`/`None` from unresolved Django types
|
|
not-subscriptable = "ignore"
|
|
|
|
# mypy already catches deprecations; ty is a fast preflight, not the authority
|
|
deprecated = "ignore"
|
|
|
|
# ty disagrees with mypy on some existing code — baselined, not suppressed.
|
|
# These rules catch real bugs in new code, so they stay enabled.
|
|
# invalid-type-form is the exception: generated code + ty limitations on valid patterns.
|
|
invalid-type-form = "ignore"
|
|
|
|
[[tool.ty.overrides]]
|
|
# Scanner schemas are built with create_model at runtime, so every kwarg looks extra to ty
|
|
include = ["products/replay_vision/backend/tests/test_scanners.py"]
|
|
|
|
[tool.ty.overrides.rules]
|
|
pydantic-discarded-extra-argument = "ignore"
|