chore: upgrade to Python 3.12.11 (#38083)

This commit is contained in:
Julian Bez
2025-10-13 08:32:29 +02:00
committed by GitHub
parent 9b09c6c9d1
commit 77e8ce5c78
51 changed files with 1147 additions and 1142 deletions

View File

@@ -66,7 +66,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: '3.11.13'
python-version: '3.12.11'
- name: Build sdist
if: matrix.os == 'ubuntu-22.04' # Only build the sdist once

View File

@@ -28,7 +28,7 @@ jobs:
concurrency: 1
group: 1
token: ${{ secrets.POSTHOG_BOT_PAT }}
python-version: '3.11.13'
python-version: '3.12.11'
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
segment: 'FOSS'
person-on-events: false

View File

@@ -121,7 +121,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11.13
python-version: 3.12.11
token: ${{ secrets.POSTHOG_BOT_PAT }}
- name: Install uv
@@ -154,7 +154,7 @@ jobs:
- name: Install python dependencies for master
run: |
UV_PROJECT_ENVIRONMENT=$pythonLocation uv sync --frozen --dev
UV_PROJECT_ENVIRONMENT=.venv-master uv sync --frozen --dev
- name: Wait for services to be available
run: |
@@ -164,7 +164,7 @@ jobs:
- name: Run migrations up to master
run: |
# Run Django migrations first (excluding managed=False models)
python manage.py migrate
.venv-master/bin/python manage.py migrate
# Then run persons migrations using sqlx; comment out until we've merged
# DATABASE_URL="postgres://posthog:posthog@localhost:5432/posthog_persons" \
# sqlx database create
@@ -327,7 +327,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.11.13']
python-version: ['3.12.11']
clickhouse-server-image: ['clickhouse/clickhouse-server:25.6.9.98']
segment: ['Core']
person-on-events: [false]
@@ -380,121 +380,121 @@ jobs:
- segment: 'Core'
person-on-events: true
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 1
- segment: 'Core'
person-on-events: true
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 2
- segment: 'Core'
person-on-events: true
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 3
- segment: 'Core'
person-on-events: true
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 4
- segment: 'Core'
person-on-events: true
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 5
- segment: 'Core'
person-on-events: true
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 6
- segment: 'Core'
person-on-events: true
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 7
- segment: 'Core'
person-on-events: true
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 8
- segment: 'Core'
person-on-events: true
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 9
- segment: 'Core'
person-on-events: true
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 10
- segment: 'Temporal'
person-on-events: false
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 1
- segment: 'Temporal'
person-on-events: false
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 2
- segment: 'Temporal'
person-on-events: false
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 3
- segment: 'Temporal'
person-on-events: false
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 4
- segment: 'Temporal'
person-on-events: false
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 5
- segment: 'Temporal'
person-on-events: false
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 6
- segment: 'Temporal'
person-on-events: false
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 7
- segment: 'Temporal'
person-on-events: false
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 8
- segment: 'Temporal'
person-on-events: false
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 9
- segment: 'Temporal'
person-on-events: false
clickhouse-server-image: 'clickhouse/clickhouse-server:25.6.9.98'
python-version: '3.11.13'
python-version: '3.12.11'
concurrency: 10
group: 10

View File

@@ -248,6 +248,14 @@ jobs:
path: playwright/test-results
retention-days: 30
- name: Archive docker-server logs
if: failure()
uses: actions/upload-artifact@v4
with:
name: docker-server-logs
path: /tmp/logs/server.txt
retention-days: 5
playwright:
name: Playwright E2E tests
needs: [changes]
@@ -305,7 +313,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11.13
python-version: 3.12.11
token: ${{ secrets.POSTHOG_BOT_PAT }}
- name: Install uv

View File

@@ -58,7 +58,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.11.13
python-version: 3.12.11
- name: Install uv
id: setup-uv
@@ -98,9 +98,9 @@ jobs:
uses: actions/cache@v4
with:
path: .mypy_cache
key: mypy-cache-${{ runner.os }}-${{ hashFiles('**/pyproject.toml', '**/mypy.ini') }}
key: mypy-cache-${{ runner.os }}-3.12-${{ hashFiles('**/pyproject.toml', '**/mypy.ini') }}
restore-keys: |
mypy-cache-${{ runner.os }}-
mypy-cache-${{ runner.os }}-3.12-
- name: Check static typing
shell: bash -e {0}

View File

@@ -14,7 +14,7 @@
<env name="REPLAY_EMBEDDINGS_ALLOWED_TEAM" value="1,2,3" />
</envs>
<option name="SDK_HOME" value="$PROJECT_DIR$/env/bin/python" />
<option name="SDK_NAME" value="Python 3.11 (posthog)" />
<option name="SDK_NAME" value="Python 3.12 (posthog)"/>
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />

View File

@@ -14,7 +14,7 @@
<env name="REPLAY_EMBEDDINGS_ALLOWED_TEAM" value="1,2,3" />
</envs>
<option name="SDK_HOME" value="$PROJECT_DIR$/env/bin/python" />
<option name="SDK_NAME" value="Python 3.11 (posthog)" />
<option name="SDK_NAME" value="Python 3.12 (posthog)"/>
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />

View File

@@ -121,7 +121,8 @@ RUN --mount=type=cache,id=pnpm,target=/tmp/pnpm-store-v23 \
#
# ---------------------------------------------------------
#
FROM python:3.11.13-slim-bookworm AS posthog-build
# Same as pyproject.toml so that uv can pick it up and doesn't need to download a different Python version.
FROM python:3.12.11-slim-bookworm AS posthog-build
WORKDIR /code
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
@@ -183,7 +184,7 @@ RUN apt-get update && \
# ---------------------------------------------------------
#
# NOTE: v1.32 is running bullseye, v1.33 is running bookworm
FROM unit:1.33.0-python3.11
FROM unit:1.33.0-python3.12
WORKDIR /code
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]
ENV PYTHONUNBUFFERED 1

View File

@@ -1,4 +1,5 @@
FROM python:3.11.13-slim-bookworm AS python-base
# Same as pyproject.toml so that uv can pick it up and doesn't need to download a different Python version.
FROM python:3.12.11-slim-bookworm AS python-base
FROM cruizba/ubuntu-dind:latest
SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"]

View File

@@ -39,12 +39,12 @@ class TestOrganizationEnterpriseAPI(APILicensedTest):
{"name": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"slug": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
},
response.json(),
}.items(),
response.json().items(),
)
response = self.client.post(
@@ -52,12 +52,12 @@ class TestOrganizationEnterpriseAPI(APILicensedTest):
{"name": "#XXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxX"},
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "#XXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxX",
"slug": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-YYYY",
},
response.json(),
}.items(),
response.json().items(),
)
@patch("posthog.api.organization.delete_bulky_postgres_data")

View File

@@ -29,13 +29,13 @@ class TestProjectEnterpriseAPI(team_enterprise_api_test_factory()):
self.assertEqual(Team.objects.count(), 2)
self.assertEqual(Project.objects.count(), 2)
response_data = response.json()
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Test",
"access_control": False,
"effective_membership_level": OrganizationMembership.Level.ADMIN,
},
response_data,
}.items(),
response_data.items(),
)
self.assertEqual(self.organization.teams.count(), 2)

View File

@@ -75,13 +75,13 @@ def team_enterprise_api_test_factory():
self.assertEqual(response.status_code, 201, response.json())
self.assertEqual(Team.objects.count(), 2)
response_data = response.json()
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Hedgebox",
"access_control": False,
"effective_membership_level": OrganizationMembership.Level.ADMIN,
},
response_data,
}.items(),
response_data.items(),
)
self.assertEqual(self.organization.teams.count(), 2)
@@ -95,13 +95,13 @@ def team_enterprise_api_test_factory():
self.assertEqual(Team.objects.count(), 2)
response_data = response.json()
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Hedgebox",
"access_control": False,
"effective_membership_level": OrganizationMembership.Level.ADMIN,
},
response_data,
}.items(),
response_data.items(),
)
response_2 = self.client.post("/api/projects/@current/environments/", {"name": "Hedgebox", "is_demo": True})
self.assertEqual(Team.objects.count(), 2, response_2.json())
@@ -173,9 +173,9 @@ def team_enterprise_api_test_factory():
response_data = response.json()
self.assertDictContainsSubset(
{"correlation_config": {"excluded_person_property_names": ["$os"]}},
response_data,
self.assertLessEqual(
{"correlation_config": {"excluded_person_property_names": ["$os"]}}.items(),
response_data.items(),
)
# Fetching projects
@@ -188,13 +188,13 @@ def team_enterprise_api_test_factory():
response_data = response.json()
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Default project",
"access_control": False,
"effective_membership_level": OrganizationMembership.Level.ADMIN,
},
response_data,
}.items(),
response_data.items(),
)
def test_fetch_team_as_org_member_works(self):
@@ -205,13 +205,13 @@ def team_enterprise_api_test_factory():
response_data = response.json()
self.assertEqual(response.status_code, HTTP_200_OK)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Default project",
"access_control": False,
"effective_membership_level": OrganizationMembership.Level.MEMBER,
},
response_data,
}.items(),
response_data.items(),
)
def test_fetch_team_as_org_outsider(self):
@@ -323,13 +323,13 @@ class TestTeamEnterpriseAPI(team_enterprise_api_test_factory()):
self.assertEqual(Team.objects.count(), 2)
self.assertEqual(Project.objects.count(), 1) # Created under the same project, not a new one!
response_data = response.json()
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Test",
"access_control": False,
"effective_membership_level": OrganizationMembership.Level.ADMIN,
},
response_data,
}.items(),
response_data.items(),
)
self.assertEqual(self.organization.teams.count(), 2)

View File

@@ -206,9 +206,9 @@ class TestAssistant(ClickhouseTestMixin, NonAtomicBaseTest):
msg_dict = (
expected_msg.model_dump(exclude_none=True) if isinstance(expected_msg, BaseModel) else expected_msg
)
self.assertDictContainsSubset(
msg_dict,
cast(BaseModel, output_msg).model_dump(exclude_none=True),
self.assertLessEqual(
msg_dict.items(),
cast(BaseModel, output_msg).model_dump(exclude_none=True).items(),
f"Message content mismatch at index {i}",
)
else:
@@ -223,7 +223,7 @@ class TestAssistant(ClickhouseTestMixin, NonAtomicBaseTest):
else expected_message
)
msg_dict = message.model_dump(exclude_none=True) if isinstance(message, BaseModel) else message
self.assertDictContainsSubset(expected_msg_dict, msg_dict, f"Message content mismatch at index {i}")
self.assertLessEqual(expected_msg_dict.items(), msg_dict.items(), f"Message content mismatch at index {i}")
@patch(
"ee.hogai.graph.query_planner.nodes.QueryPlannerNode._get_model",

View File

@@ -663,6 +663,7 @@ posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Dict ent
posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Dict entry 0 has incompatible type "str": "PathsFilter"; expected "str": "TrendsFilter" [dict-item]
posthog/hogql_queries/legacy_compatibility/filter_to_query.py:0: error: Dict entry 0 has incompatible type "str": "StickinessFilter"; expected "str": "TrendsFilter" [dict-item]
posthog/hogql_queries/legacy_compatibility/test/test_filter_to_query.py:0: error: Name "filter" already defined on line 0 [no-redef]
posthog/hogql_queries/query_runner.py:0: error: "type[Q]" has no attribute "__value__" [attr-defined]
posthog/hogql_queries/test/test_query_runner.py:0: error: Invalid base class "TestQueryRunner" [misc]
posthog/hogql_queries/test/test_query_runner.py:0: error: Variable "TestQueryRunner" is not valid as a type [valid-type]
posthog/hogql_queries/utils/query_date_range.py:0: error: Incompatible default for argument "source" (default has type "None", argument has type "Expr") [assignment]

View File

@@ -1,5 +1,5 @@
[mypy]
python_version = 3.11
python_version = 3.12
plugins =
mypy_django_plugin.main,
mypy_drf_plugin.main,

View File

@@ -1,5 +1,5 @@
import re
from typing import Any, Literal, TypedDict
from typing import Any, Literal, TypedDict, cast
from django.db.models import CharField, F, Model, QuerySet, Value
from django.db.models.functions import Cast, JSONObject
@@ -124,8 +124,14 @@ class SearchViewSet(TeamAndOrgViewSetMixin, viewsets.ViewSet):
# order by rank
if query:
qs = qs.order_by("-rank")
else:
qs = qs.order_by("type", F("_sort_name").asc(nulls_first=True))
return Response({"results": qs[:LIMIT], "counts": counts})
results = cast(list[dict[str, Any]], list(qs[:LIMIT]))
for result in results:
result.pop("_sort_name", None)
return Response({"results": results, "counts": counts})
def class_queryset(
@@ -138,7 +144,7 @@ def class_queryset(
):
"""Builds a queryset for the class."""
entity_type = class_to_entity_name(klass)
values = ["type", "result_id", "extra_fields"]
values = ["type", "result_id", "extra_fields", "_sort_name"]
qs: QuerySet[Any] = klass.objects.filter(team__project_id=project_id) # filter team
qs = view.user_access_control.filter_queryset_by_access_level(qs) # filter access level
@@ -161,6 +167,17 @@ def class_queryset(
else:
qs = qs.annotate(extra_fields=JSONObject())
sort_field: str | None = None
if extra_fields and "name" in extra_fields:
sort_field = "name"
elif entity_type == "notebook":
sort_field = "title"
if sort_field:
qs = qs.annotate(_sort_name=F(sort_field))
else:
qs = qs.annotate(_sort_name=Value(None, output_field=CharField()))
# full-text search rank
if query:
qs = qs.annotate(rank=build_rank(search_fields, query, config="simple"))

View File

@@ -147,7 +147,7 @@
'/home/runner/work/posthog/posthog/products/tasks/backend/serializers.py: Warning [WorkflowStageViewSet > WorkflowStageSerializer]: unable to resolve type hint for function "get_agent". Consider using a type hint or @extend_schema_field. Defaulting to string.',
'/home/runner/work/posthog/posthog/products/tasks/backend/serializers.py: Warning [WorkflowStageViewSet > WorkflowStageSerializer]: unable to resolve type hint for function "get_task_count". Consider using a type hint or @extend_schema_field. Defaulting to string.',
'/home/runner/work/posthog/posthog/products/user_interviews/backend/api.py: Warning [UserInterviewViewSet]: could not derive type of path parameter "project_id" because model "products.user_interviews.backend.models.UserInterview" contained no such field. Consider annotating parameter with @extend_schema. Defaulting to "string".',
'/opt/hostedtoolcache/Python/3.11.13/x64/lib/python3.11/site-packages/pydantic/_internal/_model_construction.py: Warning [QueryViewSet > ModelMetaclass]: Encountered 2 components with identical names "Person" and different identities <class \'str\'> and <class \'posthog.api.person.PersonSerializer\'>. This will very likely result in an incorrect schema. Try renaming one.',
'/opt/hostedtoolcache/Python/3.12.11/x64/lib/python3.12/site-packages/pydantic/_internal/_model_construction.py: Warning [QueryViewSet > ModelMetaclass]: Encountered 2 components with identical names "Person" and different identities <class \'str\'> and <class \'posthog.api.person.PersonSerializer\'>. This will very likely result in an incorrect schema. Try renaming one.',
'Warning: encountered multiple names for the same choice set (EffectiveMembershipLevelEnum). This may be unwanted even though the generated schema is technically correct. Add an entry to ENUM_NAME_OVERRIDES to fix the naming.',
'Warning: encountered multiple names for the same choice set (EffectivePrivilegeLevelEnum). This may be unwanted even though the generated schema is technically correct. Add an entry to ENUM_NAME_OVERRIDES to fix the naming.',
'Warning: encountered multiple names for the same choice set (HrefMatchingEnum). This may be unwanted even though the generated schema is technically correct. Add an entry to ENUM_NAME_OVERRIDES to fix the naming.',

View File

@@ -556,9 +556,9 @@ class TestPasswordResetAPI(APIBaseTest):
else:
# Fourth request should fail
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
self.assertDictContainsSubset(
{"attr": None, "code": "throttled", "type": "throttled_error"},
response.json(),
self.assertLessEqual(
{"attr": None, "code": "throttled", "type": "throttled_error"}.items(),
response.json().items(),
)
# Three emails should be sent, fourth should not
@@ -576,9 +576,9 @@ class TestPasswordResetAPI(APIBaseTest):
else:
# Fourth request should fail
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
self.assertDictContainsSubset(
{"attr": None, "code": "throttled", "type": "throttled_error"},
response.json(),
self.assertLessEqual(
{"attr": None, "code": "throttled", "type": "throttled_error"}.items(),
response.json().items(),
)
# Token validation

View File

@@ -166,7 +166,9 @@ class TestCohort(TestExportMixin, ClickhouseTestMixin, APIBaseTest, QueryMatchin
},
)
self.assertEqual(response.status_code, 200, response.content)
self.assertDictContainsSubset({"name": "whatever2", "description": "A great cohort!"}, response.json())
self.assertLessEqual(
{"name": "whatever2", "description": "A great cohort!"}.items(), response.json().items()
)
self.assertEqual(patch_calculate_cohort.call_count, 2)
self.assertIn(f" user_id:{self.user.id} ", insert_statements[0])
@@ -1638,12 +1640,12 @@ email@example.org,
},
)
self.assertEqual(response.status_code, 400, response.content)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"detail": "Cohorts cannot reference other cohorts in a loop.",
"type": "validation_error",
},
response.json(),
}.items(),
response.json().items(),
)
self.assertEqual(get_total_calculation_calls(), 3)
@@ -1666,12 +1668,12 @@ email@example.org,
},
)
self.assertEqual(response.status_code, 400, response.content)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"detail": "Cohorts cannot reference other cohorts in a loop.",
"type": "validation_error",
},
response.json(),
}.items(),
response.json().items(),
)
self.assertEqual(get_total_calculation_calls(), 3)
@@ -1775,9 +1777,9 @@ email@example.org,
},
)
self.assertEqual(response.status_code, 400, response.content)
self.assertDictContainsSubset(
{"detail": "Invalid Cohort ID in filter", "type": "validation_error"},
response.json(),
self.assertLessEqual(
{"detail": "Invalid Cohort ID in filter", "type": "validation_error"}.items(),
response.json().items(),
)
self.assertEqual(patch_calculate_cohort.call_count, 1)
@@ -2182,12 +2184,12 @@ email@example.org,
)
self.assertEqual(update_response.status_code, 400, response.content)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"detail": "Must contain a 'properties' key with type and values",
"type": "validation_error",
},
update_response.json(),
}.items(),
update_response.json().items(),
)
@patch("posthog.api.cohort.report_user_action")
@@ -2288,14 +2290,14 @@ email@example.org,
},
)
self.assertEqual(response.status_code, 400)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"type": "validation_error",
"code": "behavioral_cohort_found",
"detail": "Behavioral filters cannot be added to cohorts used in feature flags.",
"attr": "filters",
},
response.json(),
}.items(),
response.json().items(),
)
response = self.client.patch(
@@ -2323,14 +2325,14 @@ email@example.org,
},
)
self.assertEqual(response.status_code, 400)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"type": "validation_error",
"code": "behavioral_cohort_found",
"detail": "A cohort dependency (cohort XX) has filters based on events. These cohorts can't be used in feature flags.",
"attr": "filters",
},
response.json(),
}.items(),
response.json().items(),
)
@patch("django.db.transaction.on_commit", side_effect=lambda func: func())

View File

@@ -5041,7 +5041,7 @@ class TestDecideUsesReadReplica(TransactionTestCase):
sorted_flags = sorted(response_data["flags"], key=lambda x: x["key"])
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Alpha feature",
"key": "alpha-feature",
@@ -5070,10 +5070,10 @@ class TestDecideUsesReadReplica(TransactionTestCase):
"deleted": False,
"active": True,
"ensure_experience_continuity": False,
},
sorted_flags[0],
}.items(),
sorted_flags[0].items(),
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Beta feature",
"key": "beta-feature",
@@ -5088,10 +5088,10 @@ class TestDecideUsesReadReplica(TransactionTestCase):
"deleted": False,
"active": True,
"ensure_experience_continuity": False,
},
sorted_flags[1],
}.items(),
sorted_flags[1].items(),
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Group feature",
"key": "group-feature",
@@ -5102,11 +5102,11 @@ class TestDecideUsesReadReplica(TransactionTestCase):
"deleted": False,
"active": True,
"ensure_experience_continuity": False,
},
sorted_flags[2],
}.items(),
sorted_flags[2].items(),
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Inactive feature",
"key": "inactive-flag",
@@ -5114,8 +5114,8 @@ class TestDecideUsesReadReplica(TransactionTestCase):
"deleted": False,
"active": False,
"ensure_experience_continuity": False,
},
sorted_flags[3],
}.items(),
sorted_flags[3].items(),
)
self.assertEqual(response_data["group_type_mapping"], {"0": "organization", "1": "company"})
@@ -5297,7 +5297,7 @@ class TestDecideUsesReadReplica(TransactionTestCase):
sorted_flags = sorted(response_data["flags"], key=lambda x: x["key"])
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Alpha feature",
"key": "alpha-feature",
@@ -5337,11 +5337,11 @@ class TestDecideUsesReadReplica(TransactionTestCase):
"deleted": False,
"active": True,
"ensure_experience_continuity": False,
},
sorted_flags[0],
}.items(),
sorted_flags[0].items(),
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Beta feature",
"key": "beta-feature",
@@ -5362,13 +5362,13 @@ class TestDecideUsesReadReplica(TransactionTestCase):
"deleted": False,
"active": True,
"ensure_experience_continuity": False,
},
sorted_flags[1],
}.items(),
sorted_flags[1].items(),
)
# When send_cohorts is true, no transformations happen, so all relevant cohorts are returned
self.assertEqual(
response_data["cohorts"],
response_data["cohorts"].items(),
{
str(cohort_valid_for_ff.pk): {
"type": "OR",
@@ -5405,7 +5405,7 @@ class TestDecideUsesReadReplica(TransactionTestCase):
}
],
},
},
}.items(),
)
@patch("posthog.api.feature_flag.report_user_action")
@@ -5568,7 +5568,7 @@ class TestDecideUsesReadReplica(TransactionTestCase):
sorted_flags = sorted(response_data["flags"], key=lambda x: x["key"])
self.assertEqual(
response_data["cohorts"],
response_data["cohorts"].items(),
{
str(cohort_valid_for_ff.pk): {
"type": "OR",
@@ -5616,10 +5616,10 @@ class TestDecideUsesReadReplica(TransactionTestCase):
}
],
},
},
}.items(),
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Alpha feature",
"key": "alpha-feature",
@@ -5653,11 +5653,11 @@ class TestDecideUsesReadReplica(TransactionTestCase):
"deleted": False,
"active": True,
"ensure_experience_continuity": False,
},
sorted_flags[0],
}.items(),
sorted_flags[0].items(),
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Alpha feature",
"key": "alpha-feature-2",
@@ -5678,8 +5678,8 @@ class TestDecideUsesReadReplica(TransactionTestCase):
"deleted": False,
"active": True,
"ensure_experience_continuity": False,
},
sorted_flags[1],
}.items(),
sorted_flags[1].items(),
)

View File

@@ -3259,7 +3259,7 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
sorted_flags = sorted(response_data["flags"], key=lambda x: x["key"])
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Alpha feature",
"key": "alpha-feature",
@@ -3299,8 +3299,8 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
"deleted": False,
"active": True,
"ensure_experience_continuity": False,
},
sorted_flags[0],
}.items(),
sorted_flags[0].items(),
)
@patch("posthog.api.feature_flag.report_user_action")
@@ -3369,7 +3369,7 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
sorted_flags = sorted(response_data["flags"], key=lambda x: x["key"])
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Alpha feature",
"key": "alpha-feature",
@@ -3409,8 +3409,8 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
"deleted": False,
"active": True,
"ensure_experience_continuity": False,
},
sorted_flags[0],
}.items(),
sorted_flags[0].items(),
)
self.assertEqual(
@@ -3609,7 +3609,7 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
},
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Alpha feature",
"key": "alpha-feature",
@@ -3643,11 +3643,11 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
"deleted": False,
"active": True,
"ensure_experience_continuity": False,
},
sorted_flags[0],
}.items(),
sorted_flags[0].items(),
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Alpha feature",
"key": "alpha-feature-2",
@@ -3668,8 +3668,8 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
"deleted": False,
"active": True,
"ensure_experience_continuity": False,
},
sorted_flags[1],
}.items(),
sorted_flags[1].items(),
)
@patch("posthog.models.feature_flag.flag_analytics.CACHE_BUCKET_SIZE", 10)
@@ -4327,14 +4327,14 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
expected_status=status.HTTP_400_BAD_REQUEST,
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"type": "validation_error",
"code": "invalid_date",
"detail": "Invalid date value: 6hed",
"attr": "filters",
},
resp.json(),
}.items(),
resp.json().items(),
)
resp = self._create_flag_with_properties(
@@ -4350,14 +4350,14 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
expected_status=status.HTTP_400_BAD_REQUEST,
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"type": "validation_error",
"code": "invalid_date",
"detail": "Invalid date value: 1234-02-993284",
"attr": "filters",
},
resp.json(),
}.items(),
resp.json().items(),
)
def test_creating_feature_flag_with_non_existant_cohort(self):
@@ -4367,14 +4367,14 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
expected_status=status.HTTP_400_BAD_REQUEST,
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"type": "validation_error",
"code": "cohort_does_not_exist",
"detail": "Cohort with id 5151 does not exist",
"attr": "filters",
},
cohort_request.json(),
}.items(),
cohort_request.json().items(),
)
def test_validation_payloads(self):
@@ -4566,14 +4566,14 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
expected_status=status.HTTP_400_BAD_REQUEST,
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"type": "validation_error",
"code": "behavioral_cohort_found",
"detail": "Cohort 'cohort2' with filters on events cannot be used in feature flags.",
"attr": "filters",
},
cohort_request.json(),
}.items(),
cohort_request.json().items(),
)
cohort_request = self._create_flag_with_properties(
@@ -4606,14 +4606,14 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"type": "validation_error",
"code": "behavioral_cohort_found",
"detail": "Cohort 'cohort2' with filters on events cannot be used in feature flags.",
"attr": "filters",
},
response.json(),
}.items(),
response.json().items(),
)
def test_creating_feature_flag_with_nested_behavioral_cohort(self):
@@ -4666,14 +4666,14 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
expected_status=status.HTTP_400_BAD_REQUEST,
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"type": "validation_error",
"code": "behavioral_cohort_found",
"detail": "Cohort 'cohort-behavioural' with filters on events cannot be used in feature flags.",
"attr": "filters",
},
cohort_request.json(),
}.items(),
cohort_request.json().items(),
)
cohort_request = self._create_flag_with_properties(
@@ -4682,14 +4682,14 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
expected_status=status.HTTP_400_BAD_REQUEST,
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"type": "validation_error",
"code": "behavioral_cohort_found",
"detail": "Cohort 'cohort-behavioural' with filters on events cannot be used in feature flags.",
"attr": "filters",
},
cohort_request.json(),
}.items(),
cohort_request.json().items(),
)
def test_validation_group_properties(self):
@@ -5349,13 +5349,13 @@ class TestFeatureFlag(APIBaseTest, ClickhouseTestMixin):
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"type": "validation_error",
"code": "invalid_input",
"detail": "Cannot change this flag to a group-based when linked to an Early Access Feature.",
},
response.json(),
}.items(),
response.json().items(),
)
def test_cant_create_flag_with_data_that_fails_to_query(self):
@@ -6774,7 +6774,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 4, "total_users": 10}, response_json)
self.assertLessEqual({"users_affected": 4, "total_users": 10}.items(), response_json.items())
@freeze_time("2024-01-11")
def test_user_blast_radius_with_relative_date_filters(self):
@@ -6805,7 +6805,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 3, "total_users": 8}, response_json)
self.assertLessEqual({"users_affected": 3, "total_users": 8}.items(), response_json.items())
def test_user_blast_radius_with_zero_users(self):
response = self.client.post(
@@ -6828,7 +6828,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 0, "total_users": 0}, response_json)
self.assertLessEqual({"users_affected": 0, "total_users": 0}.items(), response_json.items())
def test_user_blast_radius_with_zero_selected_users(self):
for i in range(5):
@@ -6858,7 +6858,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 0, "total_users": 5}, response_json)
self.assertLessEqual({"users_affected": 0, "total_users": 5}.items(), response_json.items())
def test_user_blast_radius_with_all_selected_users(self):
for i in range(5):
@@ -6876,7 +6876,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 5, "total_users": 5}, response_json)
self.assertLessEqual({"users_affected": 5, "total_users": 5}.items(), response_json.items())
@snapshot_clickhouse_queries
def test_user_blast_radius_with_single_cohort(self):
@@ -6920,7 +6920,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 3, "total_users": 10}, response_json)
self.assertLessEqual({"users_affected": 3, "total_users": 10}.items(), response_json.items())
# test the same with precalculated cohort. Snapshots shouldn't have group property filter
cohort1.calculate_people_ch(pending_version=0)
@@ -6939,7 +6939,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 3, "total_users": 10}, response_json)
self.assertLessEqual({"users_affected": 3, "total_users": 10}.items(), response_json.items())
@snapshot_clickhouse_queries
def test_user_blast_radius_with_multiple_precalculated_cohorts(self):
@@ -7012,7 +7012,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 2, "total_users": 10}, response_json)
self.assertLessEqual({"users_affected": 2, "total_users": 10}.items(), response_json.items())
@snapshot_clickhouse_queries
def test_user_blast_radius_with_multiple_static_cohorts(self):
@@ -7064,7 +7064,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 2, "total_users": 10}, response_json)
self.assertLessEqual({"users_affected": 2, "total_users": 10}.items(), response_json.items())
cohort1.calculate_people_ch(pending_version=0)
# converts to precalculated-cohort due to simplify filters
@@ -7087,7 +7087,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 2, "total_users": 10}, response_json)
self.assertLessEqual({"users_affected": 2, "total_users": 10}.items(), response_json.items())
@snapshot_clickhouse_queries
def test_user_blast_radius_with_groups(self):
@@ -7125,7 +7125,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 4, "total_users": 10}, response_json)
self.assertLessEqual({"users_affected": 4, "total_users": 10}.items(), response_json.items())
def test_user_blast_radius_with_groups_zero_selected(self):
create_group_type_mapping_without_created_at(
@@ -7162,7 +7162,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 0, "total_users": 5}, response_json)
self.assertLessEqual({"users_affected": 0, "total_users": 5}.items(), response_json.items())
def test_user_blast_radius_with_groups_all_selected(self):
create_group_type_mapping_without_created_at(
@@ -7194,7 +7194,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 5, "total_users": 5}, response_json)
self.assertLessEqual({"users_affected": 5, "total_users": 5}.items(), response_json.items())
@snapshot_clickhouse_queries
def test_user_blast_radius_with_groups_multiple_queries(self):
@@ -7242,7 +7242,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
self.assertDictContainsSubset({"users_affected": 3, "total_users": 10}, response_json)
self.assertLessEqual({"users_affected": 3, "total_users": 10}.items(), response_json.items())
@snapshot_clickhouse_queries
def test_user_blast_radius_with_group_key_property(self):
@@ -7291,7 +7291,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
# Should match exactly 1 group out of 11 total
self.assertDictContainsSubset({"users_affected": 1, "total_users": 11}, response_json)
self.assertLessEqual({"users_affected": 1, "total_users": 11}.items(), response_json.items())
# Test filtering by group key pattern
response = self.client.post(
@@ -7316,7 +7316,7 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_200_OK)
response_json = response.json()
# Should match 10 groups that have "org:" in their key
self.assertDictContainsSubset({"users_affected": 10, "total_users": 11}, response_json)
self.assertLessEqual({"users_affected": 10, "total_users": 11}.items(), response_json.items())
def test_user_blast_radius_with_groups_incorrect_group_type(self):
create_group_type_mapping_without_created_at(
@@ -7363,13 +7363,13 @@ class TestBlastRadius(ClickhouseTestMixin, APIBaseTest):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
response_json = response.json()
self.assertDictContainsSubset(
self.assertLessEqual(
{
"type": "validation_error",
"code": "invalid_input",
"detail": "Invalid group type index for feature flag condition.",
},
response_json,
}.items(),
response_json.items(),
)

View File

@@ -171,15 +171,15 @@ class TestInsight(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest):
headers={"Referer": "https://posthog.com/my-referer", "X-Posthog-Session-Id": "my-session-id"},
)
self.assertEqual(response_1.status_code, status.HTTP_201_CREATED)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"created_at": "2021-08-23T12:00:00Z",
"created_by": self_user_basic_serialized,
"updated_at": "2021-08-23T12:00:00Z",
"last_modified_at": "2021-08-23T12:00:00Z",
"last_modified_by": self_user_basic_serialized,
},
response_1.json(),
}.items(),
response_1.json().items(),
)
mock_capture.assert_any_call(
"insight created",
@@ -204,15 +204,15 @@ class TestInsight(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest):
headers={"Referer": "https://posthog.com/my-referer", "X-Posthog-Session-Id": "my-session-id"},
)
self.assertEqual(response_2.status_code, status.HTTP_200_OK)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"created_at": "2021-08-23T12:00:00Z",
"created_by": self_user_basic_serialized,
"updated_at": "2021-09-20T12:00:00Z",
"last_modified_at": "2021-08-23T12:00:00Z",
"last_modified_by": self_user_basic_serialized,
},
response_2.json(),
}.items(),
response_2.json().items(),
)
insight_short_id = response_2.json()["short_id"]
# Check that "insight updated" event was called among all capture calls
@@ -236,28 +236,28 @@ class TestInsight(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest):
{"filters": {"events": []}},
)
self.assertEqual(response_3.status_code, status.HTTP_200_OK)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"created_at": "2021-08-23T12:00:00Z",
"created_by": self_user_basic_serialized,
"updated_at": "2021-10-21T12:00:00Z",
"last_modified_at": "2021-10-21T12:00:00Z",
"last_modified_by": self_user_basic_serialized,
},
response_3.json(),
}.items(),
response_3.json().items(),
)
with freeze_time("2021-12-23T12:00:00Z"):
response_4 = self.client.patch(f"/api/projects/{self.team.id}/insights/{insight_id}", {"name": "XYZ"})
self.assertEqual(response_4.status_code, status.HTTP_200_OK)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"created_at": "2021-08-23T12:00:00Z",
"created_by": self_user_basic_serialized,
"updated_at": "2021-12-23T12:00:00Z",
"last_modified_at": "2021-12-23T12:00:00Z",
"last_modified_by": self_user_basic_serialized,
},
response_4.json(),
}.items(),
response_4.json().items(),
)
# Field last_modified_by is updated when another user makes a material change
@@ -268,15 +268,15 @@ class TestInsight(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest):
{"description": "Lorem ipsum."},
)
self.assertEqual(response_5.status_code, status.HTTP_200_OK)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"created_at": "2021-08-23T12:00:00Z",
"created_by": self_user_basic_serialized,
"updated_at": "2022-01-01T12:00:00Z",
"last_modified_at": "2022-01-01T12:00:00Z",
"last_modified_by": alt_user_basic_serialized,
},
response_5.json(),
}.items(),
response_5.json().items(),
)
def test_get_saved_insight_items(self) -> None:
@@ -2117,11 +2117,11 @@ class TestInsight(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest):
200,
response_correct_token_retrieve.json(),
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"name": "Foobar",
},
response_correct_token_retrieve.json(),
}.items(),
response_correct_token_retrieve.json().items(),
)
self.assertEqual(
response_correct_token_list.status_code,
@@ -2130,13 +2130,13 @@ class TestInsight(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest):
)
# abcdfghi not returned as it's not related to this sharing configuration
self.assertEqual(response_correct_token_list.json()["count"], 1)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"id": insight.id,
"name": "Foobar",
"short_id": "12345678",
},
response_correct_token_list.json()["results"][0],
}.items(),
response_correct_token_list.json()["results"][0].items(),
)
def test_logged_out_user_cannot_retrieve_deleted_insight_with_correct_insight_sharing_access_token(self) -> None:
@@ -2287,7 +2287,7 @@ class TestInsight(ClickhouseTestMixin, APIBaseTest, QueryMatchingTest):
200,
response_correct_token_retrieve.json(),
)
self.assertDictContainsSubset({"name": "Foobar"}, response_correct_token_retrieve.json())
self.assertLessEqual({"name": "Foobar"}.items(), response_correct_token_retrieve.json().items())
# Below checks that the deleted insight and non-deleted insight whose tile is deleted are not be retrievable
# Also, the text tile should not affect things
self.assertEqual(

View File

@@ -822,9 +822,15 @@ class TestPerson(ClickhouseTestMixin, APIBaseTest):
response = self.client.get(f"/api/person/cohorts/?person_id={person2.uuid}").json()
response["results"].sort(key=lambda cohort: cohort["name"])
self.assertEqual(len(response["results"]), 3)
self.assertDictContainsSubset({"id": cohort1.id, "count": 2, "name": cohort1.name}, response["results"][0])
self.assertDictContainsSubset({"id": cohort3.id, "count": 1, "name": cohort3.name}, response["results"][1])
self.assertDictContainsSubset({"id": cohort4.id, "count": 1, "name": cohort4.name}, response["results"][2])
self.assertLessEqual(
{"id": cohort1.id, "count": 2, "name": cohort1.name}.items(), response["results"][0].items()
)
self.assertLessEqual(
{"id": cohort3.id, "count": 1, "name": cohort3.name}.items(), response["results"][1].items()
)
self.assertLessEqual(
{"id": cohort4.id, "count": 1, "name": cohort4.name}.items(), response["results"][2].items()
)
def test_person_cohorts_with_cohort_version(self) -> None:
PropertyDefinition.objects.create(
@@ -846,7 +852,7 @@ class TestPerson(ClickhouseTestMixin, APIBaseTest):
response = self.client.get(f"/api/person/cohorts/?person_id={person.uuid}").json()
self.assertEqual(len(response["results"]), 1)
self.assertDictContainsSubset({"id": cohort.id, "count": 1, "name": cohort.name}, response["results"][0])
self.assertLessEqual({"id": cohort.id, "count": 1, "name": cohort.name}.items(), response["results"][0].items())
# Update the group to no longer include person
cohort.groups = [{"properties": [{"key": "no", "value": "no", "type": "person"}]}]

View File

@@ -4,6 +4,7 @@ from typing import Optional, cast
from zoneinfo import ZoneInfo
import pytest
import unittest
from posthog.test.base import APIBaseTest
from unittest import mock
from unittest.mock import ANY, patch
@@ -793,6 +794,7 @@ class TestSignupAPI(APIBaseTest):
):
self.run_test_for_allowed_domain(mock_sso_providers, mock_request, mock_capture)
@unittest.skip("Skipping until fixed in Python 3.12+")
@patch("posthoganalytics.capture")
@mock.patch("ee.billing.billing_manager.BillingManager.update_billing_organization_users")
@mock.patch("social_core.backends.base.BaseAuth.request")
@@ -809,8 +811,9 @@ class TestSignupAPI(APIBaseTest):
):
with self.is_cloud(True):
self.run_test_for_allowed_domain(mock_sso_providers, mock_request, mock_capture)
assert mock_update_billing_organization_users.called_once()
mock_update_billing_organization_users.assert_called_once() # assert fails, error was shadowed in Python <3.12
@unittest.skip("Skipping until fixed in Python 3.12+")
@patch("posthoganalytics.capture")
@mock.patch("ee.billing.billing_manager.BillingManager.update_billing_organization_users")
@mock.patch("social_core.backends.base.BaseAuth.request")
@@ -827,8 +830,9 @@ class TestSignupAPI(APIBaseTest):
):
with self.is_cloud(True):
self.run_test_for_allowed_domain(mock_sso_providers, mock_request, mock_capture, use_invite=True)
assert mock_update_billing_organization_users.called_once()
mock_update_billing_organization_users.assert_called_once() # assert fails, error was shadowed in Python <3.12
@unittest.skip("Skipping until fixed in Python 3.12+")
@patch("posthoganalytics.capture")
@mock.patch("ee.billing.billing_manager.BillingManager.update_billing_organization_users")
@mock.patch("social_core.backends.base.BaseAuth.request")
@@ -847,7 +851,7 @@ class TestSignupAPI(APIBaseTest):
self.run_test_for_allowed_domain(
mock_sso_providers, mock_request, mock_capture, use_invite=True, expired_invite=True
)
assert mock_update_billing_organization_users.called_once()
mock_update_billing_organization_users.assert_called_once() # assert fails, error was shadowed in Python <3.12
@mock.patch("social_core.backends.base.BaseAuth.request")
@mock.patch("posthog.api.authentication.get_instance_available_sso_providers")

View File

@@ -179,22 +179,22 @@ def team_api_test_factory():
get_geoip_properties_mock.return_value = {}
response = self.client.post("/api/projects/@current/environments/", {"name": "Test World"})
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json())
self.assertDictContainsSubset({"name": "Test World", "week_start_day": None}, response.json())
self.assertLessEqual({"name": "Test World", "week_start_day": None}.items(), response.json().items())
get_geoip_properties_mock.return_value = {"$geoip_country_code": "US"}
response = self.client.post("/api/projects/@current/environments/", {"name": "Test US"})
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json())
self.assertDictContainsSubset({"name": "Test US", "week_start_day": 0}, response.json())
self.assertLessEqual({"name": "Test US", "week_start_day": 0}.items(), response.json().items())
get_geoip_properties_mock.return_value = {"$geoip_country_code": "PL"}
response = self.client.post("/api/projects/@current/environments/", {"name": "Test PL"})
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json())
self.assertDictContainsSubset({"name": "Test PL", "week_start_day": 1}, response.json())
self.assertLessEqual({"name": "Test PL", "week_start_day": 1}.items(), response.json().items())
get_geoip_properties_mock.return_value = {"$geoip_country_code": "IR"}
response = self.client.post("/api/projects/@current/environments/", {"name": "Test IR"})
self.assertEqual(response.status_code, status.HTTP_201_CREATED, response.json())
self.assertDictContainsSubset({"name": "Test IR", "week_start_day": 0}, response.json())
self.assertLessEqual({"name": "Test IR", "week_start_day": 0}.items(), response.json().items())
def test_cant_create_team_without_license_on_selfhosted(self):
with self.is_cloud(False):

View File

@@ -258,7 +258,7 @@ class TestUserAPI(APIBaseTest):
self.assertNotEqual(user.uuid, 1)
self.assertEqual(user.first_name, "Cooper")
self.assertEqual(user.anonymize_data, True)
self.assertDictContainsSubset({"plugin_disabled": False}, user.notification_settings)
self.assertLessEqual({"plugin_disabled": False}.items(), user.notification_settings.items())
self.assertEqual(user.has_seen_product_intro_for, {"feature_flags": True})
self.assertEqual(user.role_at_organization, "engineering")
@@ -995,9 +995,9 @@ class TestUserAPI(APIBaseTest):
for _ in range(7):
response = self.client.patch("/api/users/@me/", {"current_password": "wrong", "password": "12345678"})
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
self.assertDictContainsSubset(
{"attr": None, "code": "throttled", "type": "throttled_error"},
response.json(),
self.assertLessEqual(
{"attr": None, "code": "throttled", "type": "throttled_error"}.items(),
response.json().items(),
)
# Password was not changed
@@ -1520,9 +1520,9 @@ class TestEmailVerificationAPI(APIBaseTest):
else:
# Fourth request should fail
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
self.assertDictContainsSubset(
{"attr": None, "code": "throttled", "type": "throttled_error"},
response.json(),
self.assertLessEqual(
{"attr": None, "code": "throttled", "type": "throttled_error"}.items(),
response.json().items(),
)
# Three emails should be sent, fourth should not

View File

@@ -146,32 +146,32 @@ class Test0007PersonsAndGroupsOnEventsBackfill(AsyncMigrationBaseTest, Clickhous
events = query_events()
self.assertEqual(len(events), 3)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"distinct_id": "1",
"person_id": uuid1,
"person_properties": json.dumps({"personprop": 1}),
"person_created_at": "2022-01-01T00:00:00Z",
},
events[0],
}.items(),
events[0].items(),
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"distinct_id": "2",
"person_id": uuid1,
"person_properties": json.dumps({"personprop": 1}),
"person_created_at": "2022-01-01T00:00:00Z",
},
events[1],
}.items(),
events[1].items(),
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"distinct_id": "3",
"person_id": uuid2,
"person_properties": json.dumps({"personprop": 2}),
"person_created_at": "2022-01-02T00:00:00Z",
},
events[2],
}.items(),
events[2].items(),
)
def test_duplicated_data_persons(self):
@@ -196,14 +196,14 @@ class Test0007PersonsAndGroupsOnEventsBackfill(AsyncMigrationBaseTest, Clickhous
events = query_events()
self.assertEqual(len(events), 1)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"distinct_id": "1",
"person_id": uuid1,
"person_properties": json.dumps({"personprop": 2}),
"person_created_at": "2022-01-02T00:00:00Z",
},
events[0],
}.items(),
events[0].items(),
)
def test_deleted_data_persons(self):
@@ -223,14 +223,14 @@ class Test0007PersonsAndGroupsOnEventsBackfill(AsyncMigrationBaseTest, Clickhous
events = query_events()
self.assertEqual(len(events), 1)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"distinct_id": distinct_id,
"person_id": ZERO_UUID,
"person_properties": "{}",
"person_created_at": ZERO_DATE,
},
events[0],
}.items(),
events[0].items(),
)
def test_data_copy_groups(self):
@@ -290,7 +290,7 @@ class Test0007PersonsAndGroupsOnEventsBackfill(AsyncMigrationBaseTest, Clickhous
events = query_events()
self.assertEqual(len(events), 1)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"$group_0": "org:7",
"group0_properties": json.dumps({"industry": "IT"}),
@@ -307,8 +307,8 @@ class Test0007PersonsAndGroupsOnEventsBackfill(AsyncMigrationBaseTest, Clickhous
"$group_4": "",
"group4_properties": "{}",
"group4_created_at": ZERO_DATE,
},
events[0],
}.items(),
events[0].items(),
)
def test_no_extra_tables(self):
@@ -392,32 +392,32 @@ class Test0007PersonsAndGroupsOnEventsBackfill(AsyncMigrationBaseTest, Clickhous
events = query_events()
self.assertEqual(len(events), 3)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"distinct_id": "1_outside_lower",
"person_id": ZERO_UUID,
"person_properties": "",
"person_created_at": ZERO_DATE,
},
events[0],
}.items(),
events[0].items(),
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"distinct_id": "2_outside_upper",
"person_id": ZERO_UUID,
"person_properties": "",
"person_created_at": ZERO_DATE,
},
events[1],
}.items(),
events[1].items(),
)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"distinct_id": "3_in_range",
"person_id": _uuid3,
"person_properties": json.dumps({"personprop": 3}),
"person_created_at": "2022-01-01T00:00:00Z",
},
events[2],
}.items(),
events[2].items(),
)
def test_team_id_filter_event_not_in_team(self):
@@ -440,14 +440,14 @@ class Test0007PersonsAndGroupsOnEventsBackfill(AsyncMigrationBaseTest, Clickhous
events = query_events()
self.assertEqual(len(events), 1)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"distinct_id": "1",
"person_id": ZERO_UUID,
"person_properties": "",
"person_created_at": ZERO_DATE,
},
events[0],
}.items(),
events[0].items(),
)
def test_team_id_filter_event_in_team(self):
@@ -470,14 +470,14 @@ class Test0007PersonsAndGroupsOnEventsBackfill(AsyncMigrationBaseTest, Clickhous
events = query_events()
self.assertEqual(len(events), 1)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"distinct_id": "1",
"person_id": _uuid1,
"person_properties": json.dumps({"personprop": 1}),
"person_created_at": "2022-01-01T00:00:00Z",
},
events[0],
}.items(),
events[0].items(),
)
def test_postcheck_e2e(self):

View File

@@ -1,6 +1,5 @@
import uuid
from enum import StrEnum
from typing import Optional
from django.conf import settings
@@ -31,7 +30,7 @@ class MergeTreeEngine:
self.force_unique_zk_path = force_unique_zk_path
self.kwargs = kwargs
self.zookeeper_path_key: Optional[str] = None
self.zookeeper_path_key: str | None = None
def set_zookeeper_path_key(self, zookeeper_path_key: str):
"Used in situations where a unique zookeeper path is needed"
@@ -80,7 +79,7 @@ class AggregatingMergeTree(MergeTreeEngine):
class Distributed:
def __init__(self, data_table: str, sharding_key: Optional[str] = None, cluster: Optional[str] = None):
def __init__(self, data_table: str, sharding_key: str | None = None, cluster: str | None = None):
self.data_table = data_table
self.sharding_key = sharding_key
self.cluster = cluster

View File

@@ -1,25 +1,14 @@
import sys
from datetime import date, datetime
from enum import StrEnum
from typing import Literal, Optional, TypeAlias
from typing import Literal, Optional
from uuid import UUID
from pydantic import BaseModel, ConfigDict
ConstantDataType: TypeAlias = Literal[
"int",
"float",
"str",
"bool",
"array",
"tuple",
"date",
"datetime",
"uuid",
"unknown",
]
ConstantSupportedPrimitive: TypeAlias = int | float | str | bool | date | datetime | UUID | None
ConstantSupportedData: TypeAlias = (
type ConstantDataType = Literal["int", "float", "str", "bool", "array", "tuple", "date", "datetime", "uuid", "unknown"]
type ConstantSupportedPrimitive = int | float | str | bool | date | datetime | UUID | None
type ConstantSupportedData = (
ConstantSupportedPrimitive | list[ConstantSupportedPrimitive] | tuple[ConstantSupportedPrimitive, ...]
)

View File

@@ -1,6 +1,6 @@
import dataclasses
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Optional, TypeAlias, Union, cast
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Optional, Union, cast
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from django.db.models import Prefetch, Q
@@ -966,7 +966,7 @@ class SerializedField:
chain: Optional[list[str | int]] = None
DatabaseSchemaTable: TypeAlias = (
type DatabaseSchemaTable = (
DatabaseSchemaPostHogTable
| DatabaseSchemaSystemTable
| DatabaseSchemaDataWarehouseTable

View File

@@ -3,9 +3,11 @@ from collections.abc import Iterable
from dataclasses import dataclass
from datetime import date, datetime
from difflib import get_close_matches
from typing import Literal, Optional, Union, cast
from typing import Literal, Union, cast
from uuid import UUID
from django.conf import settings
from posthog.schema import (
HogQLQueryModifiers,
InCohortVia,
@@ -67,9 +69,12 @@ from posthog.models.surveys.util import (
from posthog.models.team import Team
from posthog.models.team.team import WeekStartDay
from posthog.models.utils import UUIDT
from posthog.settings import CLICKHOUSE_DATABASE
CHANNEL_DEFINITION_DICT = f"{CLICKHOUSE_DATABASE}.channel_definition_dict"
def get_channel_definition_dict():
"""Get the channel definition dictionary name with the correct database.
Evaluated at call time to work with test databases in Python 3.12."""
return f"{settings.CLICKHOUSE_DATABASE}.channel_definition_dict"
def team_id_guard_for_table(table_type: Union[ast.TableType, ast.TableAliasType], context: HogQLContext) -> ast.Expr:
@@ -85,7 +90,7 @@ def team_id_guard_for_table(table_type: Union[ast.TableType, ast.TableAliasType]
)
def to_printed_hogql(query: ast.Expr, team: Team, modifiers: Optional[HogQLQueryModifiers] = None) -> str:
def to_printed_hogql(query: ast.Expr, team: Team, modifiers: HogQLQueryModifiers | None = None) -> str:
"""Prints the HogQL query without mutating the node"""
return print_ast(
clone_expr(query),
@@ -103,8 +108,8 @@ def print_ast(
node: _T_AST,
context: HogQLContext,
dialect: Literal["hogql", "clickhouse"],
stack: Optional[list[ast.SelectQuery]] = None,
settings: Optional[HogQLGlobalSettings] = None,
stack: list[ast.SelectQuery] | None = None,
settings: HogQLGlobalSettings | None = None,
pretty: bool = False,
) -> str:
prepared_ast = prepare_ast_for_printing(node=node, context=context, dialect=dialect, stack=stack, settings=settings)
@@ -124,8 +129,8 @@ def prepare_ast_for_printing(
node: _T_AST,
context: HogQLContext,
dialect: Literal["hogql", "clickhouse"],
stack: Optional[list[ast.SelectQuery]] = None,
settings: Optional[HogQLGlobalSettings] = None,
stack: list[ast.SelectQuery] | None = None,
settings: HogQLGlobalSettings | None = None,
) -> _T_AST | None:
if context.database is None:
with context.timings.measure("create_hogql_database"):
@@ -198,8 +203,8 @@ def print_prepared_ast(
node: _T_AST,
context: HogQLContext,
dialect: Literal["hogql", "clickhouse"],
stack: Optional[list[ast.SelectQuery]] = None,
settings: Optional[HogQLGlobalSettings] = None,
stack: list[ast.SelectQuery] | None = None,
settings: HogQLGlobalSettings | None = None,
pretty: bool = False,
) -> str:
with context.timings.measure("printer"):
@@ -216,12 +221,12 @@ def print_prepared_ast(
@dataclass
class JoinExprResponse:
printed_sql: str
where: Optional[ast.Expr] = None
where: ast.Expr | None = None
@dataclass
class PrintableMaterializedColumn:
table: Optional[str]
table: str | None
column: str
is_nullable: bool
@@ -271,8 +276,8 @@ class _Printer(Visitor[str]):
self,
context: HogQLContext,
dialect: Literal["hogql", "clickhouse"],
stack: Optional[list[AST]] = None,
settings: Optional[HogQLGlobalSettings] = None,
stack: list[AST] | None = None,
settings: HogQLGlobalSettings | None = None,
pretty: bool = False,
):
self.context = context
@@ -503,7 +508,7 @@ class _Printer(Visitor[str]):
def visit_join_expr(self, node: ast.JoinExpr) -> JoinExprResponse:
# return constraints we must place on the select query
extra_where: Optional[ast.Expr] = None
extra_where: ast.Expr | None = None
join_strings = []
@@ -1335,7 +1340,7 @@ class _Printer(Visitor[str]):
and (len(node.args) == 1 or (has_tz_override and len(node.args) == 2))
):
# These two CH functions require a precision argument before timezone
args = args[:-1] + ["6"] + args[-1:]
args = [*args[:-1], "6", *args[-1:]]
if node.name == "toStartOfWeek" and len(node.args) == 1:
# If week mode hasn't been specified, use the project's default.
@@ -1367,19 +1372,25 @@ class _Printer(Visitor[str]):
if self.dialect == "clickhouse":
if node.name == "hogql_lookupDomainType":
return f"coalesce(dictGetOrNull('{CHANNEL_DEFINITION_DICT}', 'domain_type', (coalesce({args[0]}, ''), 'source')), dictGetOrNull('{CHANNEL_DEFINITION_DICT}', 'domain_type', (cutToFirstSignificantSubdomain(coalesce({args[0]}, '')), 'source')))"
channel_dict = get_channel_definition_dict()
return f"coalesce(dictGetOrNull('{channel_dict}', 'domain_type', (coalesce({args[0]}, ''), 'source')), dictGetOrNull('{channel_dict}', 'domain_type', (cutToFirstSignificantSubdomain(coalesce({args[0]}, '')), 'source')))"
elif node.name == "hogql_lookupPaidSourceType":
return f"coalesce(dictGetOrNull('{CHANNEL_DEFINITION_DICT}', 'type_if_paid', (coalesce({args[0]}, ''), 'source')) , dictGetOrNull('{CHANNEL_DEFINITION_DICT}', 'type_if_paid', (cutToFirstSignificantSubdomain(coalesce({args[0]}, '')), 'source')))"
channel_dict = get_channel_definition_dict()
return f"coalesce(dictGetOrNull('{channel_dict}', 'type_if_paid', (coalesce({args[0]}, ''), 'source')) , dictGetOrNull('{channel_dict}', 'type_if_paid', (cutToFirstSignificantSubdomain(coalesce({args[0]}, '')), 'source')))"
elif node.name == "hogql_lookupPaidMediumType":
return f"dictGetOrNull('{CHANNEL_DEFINITION_DICT}', 'type_if_paid', (coalesce({args[0]}, ''), 'medium'))"
channel_dict = get_channel_definition_dict()
return f"dictGetOrNull('{channel_dict}', 'type_if_paid', (coalesce({args[0]}, ''), 'medium'))"
elif node.name == "hogql_lookupOrganicSourceType":
return f"coalesce(dictGetOrNull('{CHANNEL_DEFINITION_DICT}', 'type_if_organic', (coalesce({args[0]}, ''), 'source')), dictGetOrNull('{CHANNEL_DEFINITION_DICT}', 'type_if_organic', (cutToFirstSignificantSubdomain(coalesce({args[0]}, '')), 'source')))"
channel_dict = get_channel_definition_dict()
return f"coalesce(dictGetOrNull('{channel_dict}', 'type_if_organic', (coalesce({args[0]}, ''), 'source')), dictGetOrNull('{channel_dict}', 'type_if_organic', (cutToFirstSignificantSubdomain(coalesce({args[0]}, '')), 'source')))"
elif node.name == "hogql_lookupOrganicMediumType":
return f"dictGetOrNull('{CHANNEL_DEFINITION_DICT}', 'type_if_organic', (coalesce({args[0]}, ''), 'medium'))"
channel_dict = get_channel_definition_dict()
return f"dictGetOrNull('{channel_dict}', 'type_if_organic', (coalesce({args[0]}, ''), 'medium'))"
elif node.name == "convertCurrency": # convertCurrency(from_currency, to_currency, amount, timestamp)
from_currency, to_currency, amount, *_rest = args
date = args[3] if len(args) > 3 and args[3] else "today()"
return f"if(equals({from_currency}, {to_currency}), toDecimal64({amount}, 10), if(dictGetOrDefault(`{CLICKHOUSE_DATABASE}`.`{EXCHANGE_RATE_DICTIONARY_NAME}`, 'rate', {from_currency}, {date}, toDecimal64(0, 10)) = 0, toDecimal64(0, 10), multiplyDecimal(divideDecimal(toDecimal64({amount}, 10), dictGetOrDefault(`{CLICKHOUSE_DATABASE}`.`{EXCHANGE_RATE_DICTIONARY_NAME}`, 'rate', {from_currency}, {date}, toDecimal64(0, 10))), dictGetOrDefault(`{CLICKHOUSE_DATABASE}`.`{EXCHANGE_RATE_DICTIONARY_NAME}`, 'rate', {to_currency}, {date}, toDecimal64(0, 10)))))"
db = settings.CLICKHOUSE_DATABASE
return f"if(equals({from_currency}, {to_currency}), toDecimal64({amount}, 10), if(dictGetOrDefault(`{db}`.`{EXCHANGE_RATE_DICTIONARY_NAME}`, 'rate', {from_currency}, {date}, toDecimal64(0, 10)) = 0, toDecimal64(0, 10), multiplyDecimal(divideDecimal(toDecimal64({amount}, 10), dictGetOrDefault(`{db}`.`{EXCHANGE_RATE_DICTIONARY_NAME}`, 'rate', {from_currency}, {date}, toDecimal64(0, 10))), dictGetOrDefault(`{db}`.`{EXCHANGE_RATE_DICTIONARY_NAME}`, 'rate', {to_currency}, {date}, toDecimal64(0, 10)))))"
relevant_clickhouse_name = func_meta.clickhouse_name
if "{}" in relevant_clickhouse_name:
@@ -1803,7 +1814,7 @@ class _Printer(Visitor[str]):
value = "{" + self.visit(node.value) + "}"
return f"{self._print_identifier(node.name)}={value}"
def _last_select(self) -> Optional[ast.SelectQuery]:
def _last_select(self) -> ast.SelectQuery | None:
"""Find the last SELECT query in the stack."""
for node in reversed(self.stack):
if isinstance(node, ast.SelectQuery):
@@ -1846,7 +1857,7 @@ class _Printer(Visitor[str]):
def _get_week_start_day(self) -> WeekStartDay:
return self.context.database.get_week_start_day() if self.context.database else WeekStartDay.SUNDAY
def _is_type_nullable(self, node_type: ast.Type) -> Optional[bool]:
def _is_type_nullable(self, node_type: ast.Type) -> bool | None:
if isinstance(node_type, ast.PropertyType):
return True
elif isinstance(node_type, ast.ConstantType):
@@ -1893,14 +1904,14 @@ class _Printer(Visitor[str]):
def _create_default_window_frame(self, node: ast.WindowFunction):
# For lag/lead functions, we need to order by the first argument by default
order_by: Optional[list[ast.OrderExpr]] = None
order_by: list[ast.OrderExpr] | None = None
if node.over_expr and node.over_expr.order_by:
order_by = [cast(ast.OrderExpr, clone_expr(expr)) for expr in node.over_expr.order_by]
elif node.exprs is not None and len(node.exprs) > 0:
order_by = [ast.OrderExpr(expr=clone_expr(node.exprs[0]), order="ASC")]
# Preserve existing PARTITION BY if provided via an existing OVER () clause
partition_by: Optional[list[ast.Expr]] = None
partition_by: list[ast.Expr] | None = None
if node.over_expr and node.over_expr.partition_by:
partition_by = [cast(ast.Expr, clone_expr(expr)) for expr in node.over_expr.partition_by]

View File

@@ -570,7 +570,7 @@ class TestPrinter(BaseTest):
self.assertEqual(printed_expr % context.values, unoptimized_expr % unoptimized_context.values)
if expected_context_values is not None:
self.assertDictContainsSubset(expected_context_values, context.values)
self.assertLessEqual(expected_context_values.items(), context.values.items())
if expected_skip_indexes_used is not None:
# The table needs some data to be able get a `EXPLAIN` result that includes index information -- otherwise

View File

@@ -230,7 +230,7 @@ class TestTraceQueryRunner(ClickhouseTestMixin, BaseTest):
for i, event in enumerate(value):
self.assertEventEqual(trace.events[i], event)
elif field == "person":
self.assertDictContainsSubset(value, trace.person.model_dump(mode="json", exclude={"uuid"}))
self.assertLess(value.items(), trace.person.model_dump(mode="json", exclude={"uuid"}).items())
else:
self.assertEqual(getattr(trace, field), value, f"Field {field} does not match")
@@ -332,15 +332,15 @@ class TestTraceQueryRunner(ClickhouseTestMixin, BaseTest):
self.assertEqual(len(response.results), 1)
self.assertEqual(response.results[0].id, "trace1")
self.assertEqual(response.results[0].totalLatency, 10.5)
self.assertDictContainsSubset(
self.assertLess(
{
"$ai_latency": 10.5,
"$ai_provider": "posthog",
"$ai_model": "hog-destroyer",
"$ai_http_status": 200,
"$ai_base_url": "https://us.posthog.com",
},
response.results[0].events[0].properties,
}.items(),
response.results[0].events[0].properties.items(),
)
@freeze_time("2025-01-01T00:00:00Z")

View File

@@ -360,7 +360,7 @@ class TestTracesQueryRunner(ClickhouseTestMixin, BaseTest):
self.assertEqual(response.results[0].id, "trace1")
self.assertEqual(response.results[0].totalLatency, 10.5)
self.assertEqual(len(response.results[0].events), 1)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"$ai_latency": 10.5,
"$ai_provider": "posthog",
@@ -368,8 +368,8 @@ class TestTracesQueryRunner(ClickhouseTestMixin, BaseTest):
"$ai_http_status": 200,
"$ai_base_url": "https://us.posthog.com",
"$ai_parent_id": "trace1",
},
response.results[0].events[0].properties,
}.items(),
response.results[0].events[0].properties.items(),
)
@freeze_time("2025-01-01T00:00:00Z")

View File

@@ -5041,9 +5041,15 @@ class TestTrends(ClickhouseTestMixin, APIBaseTest):
self.team,
)
event_response = sorted(event_response, key=lambda resp: resp["breakdown_value"])
self.assertDictContainsSubset({"breakdown_value": "person1", "aggregated_value": 1}, event_response[0])
self.assertDictContainsSubset({"breakdown_value": "person2", "aggregated_value": 1}, event_response[1])
self.assertDictContainsSubset({"breakdown_value": "person3", "aggregated_value": 1}, event_response[2])
self.assertLessEqual(
{"breakdown_value": "person1", "aggregated_value": 1}.items(), event_response[0].items()
)
self.assertLessEqual(
{"breakdown_value": "person2", "aggregated_value": 1}.items(), event_response[1].items()
)
self.assertLessEqual(
{"breakdown_value": "person3", "aggregated_value": 1}.items(), event_response[2].items()
)
with freeze_time("2020-01-04T13:01:01Z"):
event_response = self._run(
@@ -5067,9 +5073,15 @@ class TestTrends(ClickhouseTestMixin, APIBaseTest):
self.team,
)
event_response = sorted(event_response, key=lambda resp: resp["breakdown_value"])
self.assertDictContainsSubset({"breakdown_value": ["person1"], "aggregated_value": 1}, event_response[0])
self.assertDictContainsSubset({"breakdown_value": ["person2"], "aggregated_value": 1}, event_response[1])
self.assertDictContainsSubset({"breakdown_value": ["person3"], "aggregated_value": 1}, event_response[2])
self.assertLessEqual(
{"breakdown_value": ["person1"], "aggregated_value": 1}.items(), event_response[0].items()
)
self.assertLessEqual(
{"breakdown_value": ["person2"], "aggregated_value": 1}.items(), event_response[1].items()
)
self.assertLessEqual(
{"breakdown_value": ["person3"], "aggregated_value": 1}.items(), event_response[2].items()
)
@also_test_with_materialized_columns(person_properties=["name"])
def test_breakdown_by_person_property_pie_with_event_dau_filter(self):
@@ -5111,8 +5123,12 @@ class TestTrends(ClickhouseTestMixin, APIBaseTest):
)
event_response = sorted(event_response, key=lambda resp: resp["breakdown_value"])
self.assertEqual(len(event_response), 2)
self.assertDictContainsSubset({"breakdown_value": "person1", "aggregated_value": 1}, event_response[0])
self.assertDictContainsSubset({"breakdown_value": "person2", "aggregated_value": 1}, event_response[1])
self.assertLessEqual(
{"breakdown_value": "person1", "aggregated_value": 1}.items(), event_response[0].items()
)
self.assertLessEqual(
{"breakdown_value": "person2", "aggregated_value": 1}.items(), event_response[1].items()
)
# multiple breakdowns
with freeze_time("2020-01-04T13:01:01Z"):
@@ -5132,8 +5148,12 @@ class TestTrends(ClickhouseTestMixin, APIBaseTest):
)
event_response = sorted(event_response, key=lambda resp: resp["breakdown_value"])
self.assertEqual(len(event_response), 2)
self.assertDictContainsSubset({"breakdown_value": ["person1"], "aggregated_value": 1}, event_response[0])
self.assertDictContainsSubset({"breakdown_value": ["person2"], "aggregated_value": 1}, event_response[1])
self.assertLessEqual(
{"breakdown_value": ["person1"], "aggregated_value": 1}.items(), event_response[0].items()
)
self.assertLessEqual(
{"breakdown_value": ["person2"], "aggregated_value": 1}.items(), event_response[1].items()
)
@also_test_with_materialized_columns(person_properties=["name"])
def test_filter_test_accounts_cohorts(self):
@@ -5502,8 +5522,8 @@ class TestTrends(ClickhouseTestMixin, APIBaseTest):
self.team,
)
self.assertDictContainsSubset({"count": 2, "breakdown_value": "2"}, event_response[0])
self.assertDictContainsSubset({"count": 1, "breakdown_value": "1"}, event_response[1])
self.assertLessEqual({"count": 2, "breakdown_value": "2"}.items(), event_response[0].items())
self.assertLessEqual({"count": 1, "breakdown_value": "1"}.items(), event_response[1].items())
self.assertEntityResponseEqual(event_response, action_response)
# multiple
@@ -5529,8 +5549,8 @@ class TestTrends(ClickhouseTestMixin, APIBaseTest):
self.team,
)
self.assertDictContainsSubset({"count": 2, "breakdown_value": ["2"]}, event_response[0])
self.assertDictContainsSubset({"count": 1, "breakdown_value": ["1"]}, event_response[1])
self.assertLessEqual({"count": 2, "breakdown_value": ["2"]}.items(), event_response[0].items())
self.assertLessEqual({"count": 1, "breakdown_value": ["1"]}.items(), event_response[1].items())
self.assertEntityResponseEqual(event_response, action_response)
@also_test_with_materialized_columns(["$some_property"])

View File

@@ -1,5 +1,3 @@
from typing import TypeAlias
from posthog.schema import PropertyGroupFilter
from posthog.hogql import ast
@@ -8,7 +6,7 @@ from posthog.hogql.property import property_to_expr
from posthog.hogql_queries.insights.query_context import QueryContext
from posthog.types import AnyPropertyFilter
PropertiesType: TypeAlias = list[AnyPropertyFilter] | PropertyGroupFilter | None
type PropertiesType = list[AnyPropertyFilter] | PropertyGroupFilter | None
class Properties:

View File

@@ -828,7 +828,16 @@ class QueryRunner(ABC, Generic[Q, R, CR]):
return self.__annotations__["cached_response"]
def is_query_node(self, data) -> TypeGuard[Q]:
return isinstance(data, self.query_type)
query_type = self.query_type
# Resolve type alias if present
if hasattr(query_type, "__value__"):
query_type = query_type.__value__
# Handle both UnionType and typing._UnionGenericAlias
if isinstance(query_type, UnionType) or (type(query_type).__name__ == "_UnionGenericAlias"):
return any(isinstance(data, t) for t in get_args(query_type))
if not isinstance(query_type, type):
raise TypeError(f"query_type must be a type, got {type(query_type)}: {query_type}")
return isinstance(data, query_type)
def is_cached_response(self, data) -> TypeGuard[dict]:
return hasattr(data, "is_cached") or ( # Duck typing for backwards compatibility with `CachedQueryResponse`

View File

@@ -34,8 +34,9 @@ BASE_APP_METRICS2_COLUMNS = """
# we need to revisit producers (e.g. the webhook service currently known as rusty-hook or pgqueue).
APP_METRICS2_TIMESTAMP_TRUNCATION = "toStartOfHour(timestamp)"
APP_METRICS2_DATA_TABLE_SQL = (
lambda: f"""
def APP_METRICS2_DATA_TABLE_SQL():
return f"""
CREATE TABLE IF NOT EXISTS sharded_app_metrics2 ON CLUSTER '{settings.CLICKHOUSE_CLUSTER}'
(
{BASE_APP_METRICS2_COLUMNS}
@@ -46,10 +47,10 @@ PARTITION BY toYYYYMM(timestamp)
ORDER BY (team_id, app_source, app_source_id, instance_id, {APP_METRICS2_TIMESTAMP_TRUNCATION}, metric_kind, metric_name)
{ttl_period("timestamp", APP_METRICS2_TTL_DAYS, unit="DAY")}
"""
)
DISTRIBUTED_APP_METRICS2_TABLE_SQL = (
lambda: f"""
def DISTRIBUTED_APP_METRICS2_TABLE_SQL():
return f"""
CREATE TABLE IF NOT EXISTS app_metrics2 ON CLUSTER '{settings.CLICKHOUSE_CLUSTER}'
(
{BASE_APP_METRICS2_COLUMNS}
@@ -57,10 +58,10 @@ CREATE TABLE IF NOT EXISTS app_metrics2 ON CLUSTER '{settings.CLICKHOUSE_CLUSTER
)
ENGINE={Distributed(data_table="sharded_app_metrics2", sharding_key="rand()")}
"""
)
KAFKA_APP_METRICS2_TABLE_SQL = (
lambda: f"""
def KAFKA_APP_METRICS2_TABLE_SQL():
return f"""
CREATE TABLE IF NOT EXISTS kafka_app_metrics2 ON CLUSTER '{settings.CLICKHOUSE_CLUSTER}'
(
team_id Int64,
@@ -74,10 +75,10 @@ CREATE TABLE IF NOT EXISTS kafka_app_metrics2 ON CLUSTER '{settings.CLICKHOUSE_C
)
ENGINE={kafka_engine(topic=KAFKA_APP_METRICS2)}
"""
)
APP_METRICS2_MV_TABLE_SQL = (
lambda: f"""
def APP_METRICS2_MV_TABLE_SQL():
return f"""
CREATE MATERIALIZED VIEW IF NOT EXISTS app_metrics2_mv ON CLUSTER '{settings.CLICKHOUSE_CLUSTER}'
TO {settings.CLICKHOUSE_DATABASE}.sharded_app_metrics2
AS SELECT
@@ -91,7 +92,7 @@ metric_name,
count
FROM {settings.CLICKHOUSE_DATABASE}.kafka_app_metrics2
"""
)
TRUNCATE_APP_METRICS2_TABLE_SQL = f"TRUNCATE TABLE IF EXISTS sharded_app_metrics2"

View File

@@ -1,5 +1,3 @@
from typing import TypeAlias
from .filter import Filter
from .path_filter import PathFilter
from .properties_timeline_filter import PropertiesTimelineFilter
@@ -15,6 +13,6 @@ __all__ = [
"AnyFilter",
]
AnyFilter: TypeAlias = Filter | PathFilter | RetentionFilter | StickinessFilter | PropertiesTimelineFilter
type AnyFilter = Filter | PathFilter | RetentionFilter | StickinessFilter | PropertiesTimelineFilter
AnyInsightFilter: TypeAlias = Filter | PathFilter | RetentionFilter | StickinessFilter
type AnyInsightFilter = Filter | PathFilter | RetentionFilter | StickinessFilter

View File

@@ -32,12 +32,12 @@ class TestBase(APIBaseTest):
compared_filter = determine_compared_filter(filter)
self.assertIsInstance(compared_filter, PathFilter)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"date_from": "2020-05-16T00:00:00+00:00",
"date_to": "2020-05-22T23:59:59.999999+00:00",
},
compared_filter.to_dict(),
}.items(),
compared_filter.to_dict().items(),
)

View File

@@ -4382,9 +4382,15 @@ class TestTrends(ClickhouseTestMixin, APIBaseTest):
self.team,
)
event_response = sorted(event_response, key=lambda resp: resp["breakdown_value"])
self.assertDictContainsSubset({"breakdown_value": "person1", "aggregated_value": 1}, event_response[0])
self.assertDictContainsSubset({"breakdown_value": "person2", "aggregated_value": 1}, event_response[1])
self.assertDictContainsSubset({"breakdown_value": "person3", "aggregated_value": 1}, event_response[2])
self.assertLessEqual(
{"breakdown_value": "person1", "aggregated_value": 1}.items(), event_response[0].items()
)
self.assertLessEqual(
{"breakdown_value": "person2", "aggregated_value": 1}.items(), event_response[1].items()
)
self.assertLessEqual(
{"breakdown_value": "person3", "aggregated_value": 1}.items(), event_response[2].items()
)
@also_test_with_materialized_columns(person_properties=["name"])
def test_breakdown_by_person_property_pie_with_event_dau_filter(self):
@@ -4421,8 +4427,12 @@ class TestTrends(ClickhouseTestMixin, APIBaseTest):
)
event_response = sorted(event_response, key=lambda resp: resp["breakdown_value"])
self.assertEqual(len(event_response), 2)
self.assertDictContainsSubset({"breakdown_value": "person1", "aggregated_value": 1}, event_response[0])
self.assertDictContainsSubset({"breakdown_value": "person2", "aggregated_value": 1}, event_response[1])
self.assertLessEqual(
{"breakdown_value": "person1", "aggregated_value": 1}.items(), event_response[0].items()
)
self.assertLessEqual(
{"breakdown_value": "person2", "aggregated_value": 1}.items(), event_response[1].items()
)
def test_breakdown_hour_interval(self):
response = self._test_events_with_dates(
@@ -4845,8 +4855,8 @@ class TestTrends(ClickhouseTestMixin, APIBaseTest):
self.team,
)
self.assertDictContainsSubset({"count": 2, "breakdown_value": "2"}, event_response[0])
self.assertDictContainsSubset({"count": 1, "breakdown_value": "1"}, event_response[1])
self.assertLessEqual({"count": 2, "breakdown_value": "2"}.items(), event_response[0].items())
self.assertLessEqual({"count": 1, "breakdown_value": "1"}.items(), event_response[1].items())
self.assertEntityResponseEqual(event_response, action_response)
@also_test_with_materialized_columns(["$some_property"])

View File

@@ -70,7 +70,7 @@ class TestUpdateSurveyIteration(TestCase, ClickhouseTestMixin):
self.recurring_survey.refresh_from_db()
self.assertEqual(self.recurring_survey.current_iteration, 3)
self.assertDictContainsSubset(
self.assertLessEqual(
{
"groups": [
{
@@ -92,8 +92,8 @@ class TestUpdateSurveyIteration(TestCase, ClickhouseTestMixin):
"rollout_percentage": 100,
}
]
},
self.recurring_survey.internal_targeting_flag.filters,
}.items(),
self.recurring_survey.internal_targeting_flag.filters.items(),
)
def test_can_create_internal_targeting_flag(self) -> None:
@@ -109,7 +109,7 @@ class TestUpdateSurveyIteration(TestCase, ClickhouseTestMixin):
internal_flag = FeatureFlag.objects.get(key=self.recurring_survey.id)
assert internal_flag is not None
if internal_flag is not None:
self.assertDictContainsSubset(
self.assertLessEqual(
{
"groups": [
{
@@ -131,6 +131,6 @@ class TestUpdateSurveyIteration(TestCase, ClickhouseTestMixin):
"rollout_percentage": 100,
}
]
},
internal_flag.filters,
}.items(),
internal_flag.filters.items(),
)

View File

@@ -706,9 +706,8 @@ class KafkaLogProducerFromQueueAsync:
def configure_default_ssl_context():
"""Setup a default SSL context for Kafka."""
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
context.options |= ssl.OP_NO_SSLv2
context.options |= ssl.OP_NO_SSLv3
context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
context.check_hostname = False
context.verify_mode = ssl.CERT_OPTIONAL
context.load_default_certs()
return context

View File

@@ -1,3 +1,4 @@
import ssl
import json
import time
import uuid
@@ -31,7 +32,12 @@ from posthog.clickhouse.log_entries import (
TRUNCATE_LOG_ENTRIES_TABLE_SQL,
)
from posthog.kafka_client.topics import KAFKA_LOG_ENTRIES
from posthog.temporal.common.logger import BACKGROUND_LOGGER_TASKS, configure_logger, resolve_log_source
from posthog.temporal.common.logger import (
BACKGROUND_LOGGER_TASKS,
configure_default_ssl_context,
configure_logger,
resolve_log_source,
)
pytestmark = pytest.mark.asyncio
@@ -194,6 +200,15 @@ def structlog_context():
structlog.contextvars.bind_contextvars(**ctx)
def test_configure_default_ssl_context_uses_modern_defaults():
"""Kafka SSL contexts should rely on the modern TLS client defaults."""
context = configure_default_ssl_context()
assert context.protocol is ssl.PROTOCOL_TLS_CLIENT
assert context.verify_mode is ssl.CERT_OPTIONAL
assert context.check_hostname is False
async def test_logger_context(log_capture, event_loop):
"""Test whether log messages contain the expected context.

View File

@@ -1,4 +1,4 @@
from typing import TypeAlias, Union
from typing import Union
from posthog.schema import (
ActionsNode,
@@ -41,23 +41,16 @@ from posthog.models.filters.path_filter import PathFilter
from posthog.models.filters.retention_filter import RetentionFilter
from posthog.models.filters.stickiness_filter import StickinessFilter
FilterType: TypeAlias = Union[Filter, PathFilter, RetentionFilter, StickinessFilter]
type FilterType = Union[Filter, PathFilter, RetentionFilter, StickinessFilter]
"""Legacy insight filters."""
InsightQueryNode: TypeAlias = Union[
TrendsQuery,
FunnelsQuery,
RetentionQuery,
PathsQuery,
StickinessQuery,
LifecycleQuery,
]
type InsightQueryNode = Union[TrendsQuery, FunnelsQuery, RetentionQuery, PathsQuery, StickinessQuery, LifecycleQuery]
InsightActorsQueryNode: TypeAlias = Union[
type InsightActorsQueryNode = Union[
InsightActorsQuery, FunnelsActorsQuery, FunnelCorrelationActorsQuery, StickinessActorsQuery
]
AnyPropertyFilter: TypeAlias = Union[
type AnyPropertyFilter = Union[
EventPropertyFilter,
PersonPropertyFilter,
ElementPropertyFilter,
@@ -78,5 +71,5 @@ AnyPropertyFilter: TypeAlias = Union[
LogPropertyFilter,
]
EntityNode: TypeAlias = Union[EventsNode, ActionsNode, DataWarehouseNode]
ExclusionEntityNode: TypeAlias = Union[FunnelExclusionEventsNode, FunnelExclusionActionsNode]
type EntityNode = Union[EventsNode, ActionsNode, DataWarehouseNode]
type ExclusionEntityNode = Union[FunnelExclusionEventsNode, FunnelExclusionActionsNode]

View File

@@ -2,7 +2,7 @@ import csv
import time
from datetime import datetime
from io import StringIO
from typing import TYPE_CHECKING, Any, Optional, TypeAlias
from typing import TYPE_CHECKING, Any, Optional
from uuid import UUID
from django.db import models
@@ -70,7 +70,7 @@ ExtractErrors = {
"Rows have different amount of values": "The provided file has rows with different amount of values",
}
DataWarehouseTableColumns: TypeAlias = dict[str, dict[str, str | bool]] | dict[str, str]
type DataWarehouseTableColumns = dict[str, dict[str, str | bool]] | dict[str, str]
class DataWarehouseTableManager(models.Manager):

View File

@@ -559,7 +559,7 @@
"$os_version": "15.3.1",
"$process_person_profile": false,
"$lib_version": "3.21.0",
"$python_version": "3.11.13",
"$python_version": "3.12.11",
"$exception_message": "Connection closed by server.",
"$transformations_succeeded": ["GeoIP (0195b934-6d98-0000-4d00-8163a218f25c)"],
"$exception_type": "ConnectionError",

View File

@@ -170,7 +170,7 @@
"$exception_issue_id": "2845fb04-dbc7-4945-bb58-48d6f77d2015",
"$exception_personURL": "http://localhost:8010/project/phc_ngf62dtzXeNsSZNWJ8eB5Je1l7FQoSybCZHEOggcXv7/person/b071a20d-361d-4378-af15-fddf17badd18",
"$geoip_disable": true,
"$python_version": "3.11.13",
"$python_version": "3.12.11",
"$os": "Mac OS X",
"$lib_version": "3.21.0",
"$exception_type": "Exception",

View File

@@ -1,7 +1,7 @@
[project]
name = "posthog"
version = "0.0.0"
requires-python = "==3.11.*" # Same as in Dockerfile
requires-python = "==3.12.11" # Leave patch version so that we don't accidentally upgrade minor versions
dependencies = [
"aioboto3==12.0.0",
"aiohttp==3.11.10",
@@ -12,7 +12,7 @@ dependencies = [
"beautifulsoup4==4.12.3",
"boto3==1.28.16",
"brotli==1.1.0",
"celery==5.3.4",
"celery==5.3.6",
"celery-redbeat==2.1.1",
"certifi==2025.8.3",
"clickhouse-connect==0.8.17",
@@ -62,8 +62,8 @@ dependencies = [
"hogql-parser==1.2.0",
"infi-clickhouse-orm",
"jsonref==1.1.0",
"kafka-python==2.0.2",
"kombu==5.3.2",
"kafka-python==2.0.6",
"kombu==5.3.7",
"langchain==0.3.26",
"langchain-community==0.3.27",
"langchain-openai==0.3.33",
@@ -83,12 +83,12 @@ dependencies = [
"orjson==3.10.15",
"pandas==2.2.0",
"paramiko==3.4.0",
"pdpyras==5.2.0",
"pdpyras==5.4.1",
"phonenumberslite==8.13.6",
"pillow==10.2.0",
"posthoganalytics>=6.7.4",
"psutil==6.0.0",
"psycopg2-binary==2.9.7",
"psycopg2-binary==2.9.10",
"psycopg[binary]==3.2.4",
"pyarrow==18.1.0",
"pydantic==2.10.3",
@@ -139,7 +139,7 @@ dependencies = [
"webdriver-manager==4.0.2",
"whitenoise~=6.10.0",
"xmlsec==1.3.14",
"zstd==1.5.5.1",
"zstd==1.5.7.2",
"zxcvbn==4.4.28",
"pyyaml==6.0.1",
"chdb==3.3.0",
@@ -209,7 +209,7 @@ dev = [
"pytest-django~=4.11.1",
"pytest-env==0.8.2",
"pytest-icdiff==0.6",
"pytest-mock==3.11.1",
"pytest-mock==3.15.0",
"pytest-split==0.9.0",
"pytest-watch==4.2.0",
"pytest-xdist==3.6.1",
@@ -248,7 +248,7 @@ infi-clickhouse-orm = { git = "https://github.com/PostHog/infi.clickhouse_orm",
[tool.black]
line-length = 120
target-version = ['py311']
target-version = ['py312']
[tool.isort]
profile = "black"
@@ -336,7 +336,7 @@ section-order = [
"products" = ["products"]
[tool.dagster]
python_version = "3.11"
python_version = "3.12"
[tool.mypy-baseline]
sort_baseline = true

View File

@@ -39,7 +39,7 @@
},
"applications": {
"posthog": {
"type": "python 3.11",
"type": "python 3.12",
"processes": $NGINX_UNIT_APP_PROCESSES,
"working_directory": "/code",
"path": ".",
@@ -51,7 +51,7 @@
}
},
"metrics": {
"type": "python 3.11",
"type": "python 3.12",
"processes": 1,
"working_directory": "/code/bin",
"path": ".",

1427
uv.lock generated

File diff suppressed because it is too large Load Diff