From 7167c5dd8a5efc4e048def1454e025069e448ace Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 17:08:47 +0000 Subject: [PATCH 1/7] Add native PostgreSQL support alongside SQLite Add DATABASE_ENGINE=postgres (plus DATABASE_HOST/PORT/USER/PASSWORD/NAME) config and centralize all sqlite-vs-postgres branching in archivebox.misc.db: - get_database_settings() builds DATABASES for either backend; the sqlite path is unchanged (custom lock-retry backend, same PRAGMAs). - database_exists()/ensure_database_ready() replace index.sqlite3 file checks; init auto-creates the postgres database when missing. - approximate_row_counts() serves admin index counts from sqlite_stat1 or pg_class.reltuples; missing-table detection covers both vendors. - rebuild_models_from_migration_state() lets historical sqlite-only raw SQL migrations resync postgres schema from Django migration state at every divergence point (postgres can never hold legacy data, so affected tables are empty when these run). All raw-DDL and PRAGMA migrations are now vendor-gated with sqlite behavior byte-for-byte unchanged. - A pre_save clamp truncates CharField values to max_length: sqlite never enforced varchar(n) but postgres does (e.g. long crawl labels). - Collation-sensitive URL range scans branch to escaped LIKE on postgres (with a text_pattern_ops index) since linguistic collations break bytewise range tricks; the crawl-config JSON search wave gets a jsonb-text implementation. Verified on real PostgreSQL 16: fresh init applies the entire migration graph, schema matches models exactly (column-level parity check + makemigrations --check), and add/run/list/search/status/remove all work end-to-end. New test_postgres_backend.py suite boots a real throwaway postgres cluster (initdb + pg_ctl); CI workflows install postgres server binaries. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019YHSjZM6TstSAMN2PhgfUg --- .github/workflows/test-parallel.yml | 7 + .github/workflows/test.yml | 14 + archivebox/api/migrations/0001_initial.py | 64 +- archivebox/cli/archivebox_init.py | 19 +- archivebox/cli/archivebox_status.py | 5 +- archivebox/config/common.py | 9 + archivebox/config/django.py | 7 +- archivebox/config/paths.py | 31 +- archivebox/core/admin_site.py | 31 +- archivebox/core/apps.py | 8 + .../core/migrations/0023_upgrade_to_0_9_0.py | 18 + .../migrations/0024_assign_default_crawl.py | 162 ++-- ...options_alter_snapshot_options_and_more.py | 20 + .../0027_copy_archiveresult_to_process.py | 8 + .../0029_migrate_archiveresult_to_uuid_pk.py | 17 + .../0046_repair_snapshot_permissions.py | 7 + .../0051_postgres_url_pattern_ops_index.py | 30 + archivebox/core/settings.py | 38 +- archivebox/core/views.py | 17 +- archivebox/crawls/migrations/0001_initial.py | 65 +- .../migrations/0002_upgrade_from_0_8_6.py | 4 + .../0016_hydrate_crawl_permissions.py | 6 + archivebox/machine/migrations/0001_initial.py | 57 +- .../migrations/0005_converge_binary_model.py | 7 + .../0011_remove_binary_output_dir.py | 14 +- .../0012_add_machine_config_if_missing.py | 6 + archivebox/misc/db.py | 264 ++++++- archivebox/misc/logging_util.py | 3 +- .../0004_hydrate_persona_permissions.py | 6 + archivebox/search/query.py | 41 +- archivebox/search/views.py | 19 +- archivebox/tests/test_postgres_backend.py | 228 ++++++ bin/benchmark_db_backends.py | 195 +++++ docs/Configuration.md | 47 +- pyproject.toml | 1 + uv.lock | 729 ++++++++++-------- 36 files changed, 1649 insertions(+), 555 deletions(-) create mode 100644 archivebox/core/migrations/0051_postgres_url_pattern_ops_index.py create mode 100644 archivebox/tests/test_postgres_backend.py create mode 100644 bin/benchmark_db_backends.py diff --git a/.github/workflows/test-parallel.yml b/.github/workflows/test-parallel.yml index 8ad7c5e0..0f6d3403 100644 --- a/.github/workflows/test-parallel.yml +++ b/.github/workflows/test-parallel.yml @@ -317,6 +317,13 @@ jobs: | "$JQ_BINARY" -r 'to_entries[] | "\(.key)=\(.value)"' \ >> "$GITHUB_ENV" + - name: Install PostgreSQL server binaries + run: | + set -Eeuo pipefail + if ! ls /usr/lib/postgresql/*/bin/initdb >/dev/null 2>&1 && ! command -v initdb >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y postgresql + fi + - name: Run test - ${{ matrix.test.name }} env: TEST_PATHS_JSON: ${{ toJson(matrix.test.paths) }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0fcb26c..dfd9cd8c 100755 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -205,6 +205,20 @@ jobs: uv run --directory "$DATA_DIR" --no-sync --no-sources archivebox version uv run --directory "$DATA_DIR" --no-sync --no-sources archivebox status + - name: Install PostgreSQL server binaries + if: matrix.os_name == 'macOS' || matrix.python == '3.14' + run: | + set -Eeuo pipefail + if [ "$RUNNER_OS" = "Linux" ]; then + if ! ls /usr/lib/postgresql/*/bin/initdb >/dev/null 2>&1 && ! command -v initdb >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y postgresql + fi + else + if ! command -v initdb >/dev/null 2>&1 && ! ls /opt/homebrew/opt/postgresql*/bin/initdb >/dev/null 2>&1; then + brew install postgresql@17 + fi + fi + - name: Run consolidated core suite if: matrix.os_name == 'macOS' || matrix.python == '3.14' run: | diff --git a/archivebox/api/migrations/0001_initial.py b/archivebox/api/migrations/0001_initial.py index 1f3e6f3d..99cdeef9 100644 --- a/archivebox/api/migrations/0001_initial.py +++ b/archivebox/api/migrations/0001_initial.py @@ -12,19 +12,9 @@ import signal_webhooks.fields import signal_webhooks.utils -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.SeparateDatabaseAndState( - database_operations=[ - migrations.RunSQL( - sql=""" +# Raw sqlite DDL kept EXACTLY as-is; executed only on sqlite via Django's +# RunSQL (identical statement splitting → byte-for-byte identical behavior). +INITIAL_SQL = """ -- Create api_apitoken table CREATE TABLE IF NOT EXISTS api_apitoken ( id TEXT PRIMARY KEY NOT NULL, @@ -70,12 +60,48 @@ class Migration(migrations.Migration): CREATE INDEX IF NOT EXISTS api_outboundwebhook_created_at_idx ON api_outboundwebhook(created_at); CREATE INDEX IF NOT EXISTS api_outboundwebhook_name_idx ON api_outboundwebhook(name); CREATE INDEX IF NOT EXISTS api_outboundwebhook_ref_idx ON api_outboundwebhook(ref); - """, - reverse_sql=""" + """ + +INITIAL_REVERSE_SQL = """ DROP TABLE IF EXISTS api_outboundwebhook; DROP TABLE IF EXISTS api_apitoken; - """, - ), + """ + + +def _run_sqlite_only_sql(apps, schema_editor): + if schema_editor.connection.vendor != "sqlite": + return + migrations.RunSQL(sql=INITIAL_SQL, reverse_sql=INITIAL_REVERSE_SQL).database_forwards( + "api", schema_editor, None, None, + ) + + +def _run_sqlite_only_sql_reverse(apps, schema_editor): + if schema_editor.connection.vendor != "sqlite": + return + migrations.RunSQL(sql=INITIAL_SQL, reverse_sql=INITIAL_REVERSE_SQL).database_backwards( + "api", schema_editor, None, None, + ) + + +def _pg_sync_schema(apps, schema_editor): + from archivebox.misc.db import rebuild_models_from_migration_state + + rebuild_models_from_migration_state(apps, schema_editor, "api", ["APIToken", "OutboundWebhook"]) + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.RunPython(_run_sqlite_only_sql, reverse_code=_run_sqlite_only_sql_reverse), ], state_operations=[ migrations.CreateModel( @@ -236,4 +262,8 @@ class Migration(migrations.Migration): ), ], ), + # On non-sqlite backends the raw DDL above is skipped, so the real + # schema diverges from Django state. Rebuild both tables (including the + # unique constraint) from the post-migration state (no-op on sqlite). + migrations.RunPython(_pg_sync_schema, reverse_code=migrations.RunPython.noop), ] diff --git a/archivebox/cli/archivebox_init.py b/archivebox/cli/archivebox_init.py index f20d3d16..73416ed8 100755 --- a/archivebox/cli/archivebox_init.py +++ b/archivebox/cli/archivebox_init.py @@ -28,13 +28,13 @@ def init(force: bool = False, quick: bool = False, install: bool = False) -> Non from archivebox.config import CONSTANTS, VERSION from archivebox.config.common import get_config from archivebox.config.collection import write_config_file - from archivebox.misc.db import apply_migrations + from archivebox.misc.db import apply_migrations, database_exists, ensure_database_ready from archivebox.misc.checks import check_migrations config = get_config() is_empty = not len(set(os.listdir(CONSTANTS.DATA_DIR)) - CONSTANTS.ALLOWED_IN_DATA_DIR) - existing_index = os.path.isfile(CONSTANTS.DATABASE_FILE) + existing_index = database_exists() if is_empty and not existing_index: print(f"[turquoise4][+] Initializing a new ArchiveBox v{VERSION} collection...[/turquoise4]") print("[green]----------------------------------------------------------------------[/green]") @@ -82,11 +82,15 @@ def init(force: bool = False, quick: bool = False, install: bool = False) -> Non # create the ArchiveBox.conf file write_config_file({"SECRET_KEY": config.SECRET_KEY}) - if os.access(CONSTANTS.DATABASE_FILE, os.F_OK): + if existing_index: print("\n[green][*] Verifying main SQL index and running any migrations needed...[/green]") else: print("\n[green][+] Building main SQL index and running initial migrations...[/green]") + # For postgres, make sure the server is reachable and create the database + # if it doesn't exist yet (sqlite creates its file automatically). + ensure_database_ready() + from archivebox.config.django import setup_django setup_django() @@ -103,9 +107,14 @@ def init(force: bool = False, quick: bool = False, install: bool = False) -> Non else: os.environ["ARCHIVEBOX_WANTS_INIT"] = previous_wants_init - assert os.path.isfile(CONSTANTS.DATABASE_FILE) and os.access(CONSTANTS.DATABASE_FILE, os.R_OK) + from archivebox.misc.db import database_display_location, is_postgres + + assert database_exists() print() - print(f" √ {_display_data_path(CONSTANTS.DATABASE_FILE, CONSTANTS.DATA_DIR)}") + if is_postgres(): + print(f" √ {database_display_location()}") + else: + print(f" √ {_display_data_path(CONSTANTS.DATABASE_FILE, CONSTANTS.DATA_DIR)}") print() print("[dodger_blue3][*] Checking links from indexes and archive folders (safe to Ctrl+C)...[/dodger_blue3]") diff --git a/archivebox/cli/archivebox_status.py b/archivebox/cli/archivebox_status.py index 96f8c035..e4927788 100644 --- a/archivebox/cli/archivebox_status.py +++ b/archivebox/cli/archivebox_status.py @@ -44,7 +44,10 @@ def status(out_dir: Path = CONSTANTS.DATA_DIR) -> None: legacy_snapshot_dirs = [ entry for entry in archive_dir.iterdir() if entry.is_dir() and not entry.is_symlink() and Snapshot.is_legacy_archive_dir(entry) ] - print(f" > SQL Main Index: {num_sql_links} links".ljust(36), f"(found in {CONSTANTS.SQL_INDEX_FILENAME})") + from archivebox.misc.db import database_display_location, is_postgres + + index_location = database_display_location() if is_postgres() else CONSTANTS.SQL_INDEX_FILENAME + print(f" > SQL Main Index: {num_sql_links} links".ljust(36), f"(found in {index_location})") print(f" > JSON Link Details: {len(legacy_snapshot_dirs)} links".ljust(36), f"(found in {archive_dir.name}/*/index.json)") print() print("[green]\\[*] Scanning archive data directories...[/green]") diff --git a/archivebox/config/common.py b/archivebox/config/common.py index fdb39bb0..01c4c9ec 100644 --- a/archivebox/config/common.py +++ b/archivebox/config/common.py @@ -350,7 +350,16 @@ class DatabaseConfig(BaseConfigSet): toml_section_header: str = "DATABASE_CONFIG" _scope: str = PrivateAttr(default=_SCOPE_SERVER) + DATABASE_ENGINE: str = Field( + default="sqlite", + alias="ARCHIVEBOX_DATABASE_ENGINE", + pattern=r"(?i)^(sqlite|postgres|postgresql)$", + ) DATABASE_NAME: str = Field(default=str(CONSTANTS.DATABASE_FILE), alias="ARCHIVEBOX_DATABASE_NAME") + DATABASE_HOST: str = Field(default="127.0.0.1", alias="ARCHIVEBOX_DATABASE_HOST") + DATABASE_PORT: int = Field(default=5432, alias="ARCHIVEBOX_DATABASE_PORT", ge=1, le=65535) + DATABASE_USER: str = Field(default="archivebox", alias="ARCHIVEBOX_DATABASE_USER") + DATABASE_PASSWORD: str = Field(default="", alias="ARCHIVEBOX_DATABASE_PASSWORD") SQLITE_JOURNAL_MODE: str = Field( default="WAL", alias="ARCHIVEBOX_SQLITE_JOURNAL_MODE", diff --git a/archivebox/config/django.py b/archivebox/config/django.py index 9f326522..a4626e5d 100644 --- a/archivebox/config/django.py +++ b/archivebox/config/django.py @@ -149,9 +149,10 @@ def setup_django(check_db=False, in_memory_db=False) -> None: for conn in connections.all(): conn.close_if_unusable_or_obsolete() - sql_index_path = CONSTANTS.DATABASE_FILE - assert os.access(sql_index_path, os.F_OK), ( - f"No database file {sql_index_path} found in: {CONSTANTS.DATA_DIR} (Are you in an ArchiveBox collection directory?)" + from archivebox.misc.db import database_display_location, database_exists + + assert database_exists(), ( + f"No database {database_display_location()} found for: {CONSTANTS.DATA_DIR} (Are you in an ArchiveBox collection directory?)" ) except KeyboardInterrupt: diff --git a/archivebox/config/paths.py b/archivebox/config/paths.py index 036cc550..3e5611d7 100644 --- a/archivebox/config/paths.py +++ b/archivebox/config/paths.py @@ -58,9 +58,11 @@ def _get_collection_id(DATA_DIR=DATA_DIR, force_create=False) -> str: collection_id = hashlib.sha256(f"{machine_id}:{collection_path}@{creation_date}".encode()).hexdigest()[:8] try: - # only persist collection_id file if we already have an index.sqlite3 file present + # only persist collection_id file if this dir already looks like a real collection + # (has an index.sqlite3, or an ArchiveBox.conf when the DB lives in postgres), # otherwise we might be running in a directory that is not a collection, no point creating cruft files - collection_is_active = os.path.isfile(DATABASE_FILE) and os.path.isdir(ARCHIVE_DIR) and os.access(DATA_DIR, os.W_OK) + collection_marker = os.path.isfile(DATABASE_FILE) or os.path.isfile(DATA_DIR / "ArchiveBox.conf") + collection_is_active = collection_marker and os.path.isdir(ARCHIVE_DIR) and os.access(DATA_DIR, os.W_OK) if collection_is_active or force_create: collection_id_file.write_text(collection_id) @@ -270,6 +272,24 @@ def get_or_create_working_lib_dir(autofix=True, quiet=False, config: "ArchiveBox raise OSError(f"ArchiveBox is unable to find a writable ABXPKG_LIB_DIR, tried {CANDIDATES}!") +def _sql_index_location() -> dict: + from archivebox.misc.db import database_display_location, database_exists, is_postgres + + if is_postgres(): + return { + "path": database_display_location(), + "enabled": True, + "is_valid": database_exists(), + "is_mount": False, + } + return { + "path": DATABASE_FILE.resolve(), + "enabled": True, + "is_valid": os.path.isfile(DATABASE_FILE) and os.access(DATABASE_FILE, os.R_OK) and os.access(DATABASE_FILE, os.W_OK), + "is_mount": os.path.ismount(DATABASE_FILE.resolve()), + } + + def get_data_locations(config: "ArchiveBoxConfig | None" = None, **config_kwargs): from archivebox.config.constants import CONSTANTS from archivebox.config.common import get_config @@ -296,12 +316,7 @@ def get_data_locations(config: "ArchiveBoxConfig | None" = None, **config_kwargs and os.access(CONSTANTS.CONFIG_FILE, os.R_OK) and os.access(CONSTANTS.CONFIG_FILE, os.W_OK), }, - "SQL_INDEX": { - "path": DATABASE_FILE.resolve(), - "enabled": True, - "is_valid": os.path.isfile(DATABASE_FILE) and os.access(DATABASE_FILE, os.R_OK) and os.access(DATABASE_FILE, os.W_OK), - "is_mount": os.path.ismount(DATABASE_FILE.resolve()), - }, + "SQL_INDEX": _sql_index_location(), "ARCHIVE_DIR": { "path": CONSTANTS.ARCHIVE_DIR.resolve(), "enabled": True, diff --git a/archivebox/core/admin_site.py b/archivebox/core/admin_site.py index 9df8e688..36ef51ba 100644 --- a/archivebox/core/admin_site.py +++ b/archivebox/core/admin_site.py @@ -122,8 +122,6 @@ class ArchiveBoxAdmin(admin.AdminSite): def index(self, request: "HttpRequest", extra_context: dict[str, Any] | None = None) -> "TemplateResponse": response = super().index(request, extra_context) - if connection.vendor != "sqlite": - return response models_by_table: dict[str, list[dict[str, Any]]] = {} for app in response.context_data.get("app_list", []): @@ -136,23 +134,18 @@ class ArchiveBoxAdmin(admin.AdminSite): if not models_by_table: return response - try: - with connection.cursor() as cursor: - cursor.execute("SELECT tbl, stat FROM sqlite_stat1") - for table, stat in cursor.fetchall(): - try: - count = int(str(stat).split()[0]) - except (IndexError, TypeError, ValueError): - continue - self._set_model_object_count( - models_by_table, - table, - count, - title=f"Approximate count from SQLite stats: {count:,}", - ) - models_by_table.pop(table, None) - except DatabaseError: - pass + from archivebox.misc.db import approximate_row_counts + + for table, count in approximate_row_counts(connection).items(): + if table not in models_by_table: + continue + self._set_model_object_count( + models_by_table, + table, + count, + title=f"Approximate count from database stats: {count:,}", + ) + models_by_table.pop(table, None) for table in list(models_by_table): try: diff --git a/archivebox/core/apps.py b/archivebox/core/apps.py index 774d7433..c2674b6b 100644 --- a/archivebox/core/apps.py +++ b/archivebox/core/apps.py @@ -20,6 +20,14 @@ class CoreConfig(AppConfig): ModelWithOutputDir.register_delete_signal() + # SQLite ignores VARCHAR(n) limits but PostgreSQL enforces them; clamp + # CharField values on save so writes behave the same on both backends. + from django.db.models.signals import pre_save + + from archivebox.misc.db import truncate_overlong_charfields + + pre_save.connect(truncate_overlong_charfields, dispatch_uid="archivebox_truncate_overlong_charfields") + # Import models to register state machines with the registry # Skip during makemigrations to avoid premature state machine access if "makemigrations" not in sys.argv: diff --git a/archivebox/core/migrations/0023_upgrade_to_0_9_0.py b/archivebox/core/migrations/0023_upgrade_to_0_9_0.py index 1dfb13ed..a25d9569 100644 --- a/archivebox/core/migrations/0023_upgrade_to_0_9_0.py +++ b/archivebox/core/migrations/0023_upgrade_to_0_9_0.py @@ -47,6 +47,14 @@ def normalize_status(status): def upgrade_core_tables(apps, schema_editor): """Upgrade core tables from v0.7.2 or v0.8.6rc0 to v0.9.0.""" + # sqlite-only legacy repair/rebuild. On postgres this raw SQL (PRAGMA, + # sqlite_master, INSERT OR IGNORE, DATETIME, table-rebuild dance) is both + # invalid and unnecessary: a postgres database can never contain legacy + # data at this point. The final _pg_sync_schema op resyncs the real + # postgres schema to migration state instead. + if schema_editor.connection.vendor != "sqlite": + return + from archivebox.uuid_compat import uuid7 cursor = connection.cursor() @@ -491,6 +499,15 @@ def upgrade_core_tables(apps, schema_editor): print(" ✓ Core table rebuild complete") +def _pg_sync_schema(apps, schema_editor): + # On postgres, upgrade_core_tables is a no-op above, so the raw sqlite + # rebuilds of core_tag/core_snapshot/core_archiveresult never ran. Resync + # the real schema to this migration's end-state (tables are empty on pg). + from archivebox.misc.db import rebuild_models_from_migration_state + + rebuild_models_from_migration_state(apps, schema_editor, "core", ["Tag", "Snapshot", "SnapshotTag", "ArchiveResult"]) + + class Migration(migrations.Migration): dependencies = [ ("core", "0022_auto_20231023_2008"), @@ -571,4 +588,5 @@ class Migration(migrations.Migration): ), ], ), + migrations.RunPython(_pg_sync_schema, reverse_code=migrations.RunPython.noop), ] diff --git a/archivebox/core/migrations/0024_assign_default_crawl.py b/archivebox/core/migrations/0024_assign_default_crawl.py index e32c2552..75548553 100644 --- a/archivebox/core/migrations/0024_assign_default_crawl.py +++ b/archivebox/core/migrations/0024_assign_default_crawl.py @@ -4,11 +4,102 @@ from django.db import migrations, models +# Raw sqlite table-rebuild that makes core_snapshot.crawl_id NOT NULL. Kept +# byte-for-byte and replayed through Django's own RunSQL only on sqlite. +_MAKE_CRAWL_ID_NOT_NULL_SQL = """ + -- Rebuild snapshot table with NOT NULL crawl_id + CREATE TABLE core_snapshot_final ( + id TEXT PRIMARY KEY NOT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + modified_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + + url TEXT NOT NULL, + timestamp VARCHAR(32) NOT NULL UNIQUE, + bookmarked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + + crawl_id TEXT NOT NULL, + parent_snapshot_id TEXT, + + title VARCHAR(512), + downloaded_at DATETIME, + depth INTEGER NOT NULL DEFAULT 0, + fs_version VARCHAR(10) NOT NULL DEFAULT '0.9.0', + + config TEXT NOT NULL DEFAULT '{}', + notes TEXT NOT NULL DEFAULT '', + num_uses_succeeded INTEGER NOT NULL DEFAULT 0, + num_uses_failed INTEGER NOT NULL DEFAULT 0, + + status VARCHAR(15) NOT NULL DEFAULT 'queued', + retry_at DATETIME, + current_step INTEGER NOT NULL DEFAULT 0, + + FOREIGN KEY (crawl_id) REFERENCES crawls_crawl(id) ON DELETE CASCADE, + FOREIGN KEY (parent_snapshot_id) REFERENCES core_snapshot(id) ON DELETE SET NULL + ); + + INSERT INTO core_snapshot_final ( + id, url, timestamp, title, + bookmarked_at, created_at, modified_at, + crawl_id, parent_snapshot_id, + downloaded_at, depth, fs_version, + config, notes, + num_uses_succeeded, num_uses_failed, + status, retry_at, current_step + ) + SELECT + id, url, timestamp, title, + bookmarked_at, created_at, modified_at, + REPLACE(crawl_id, '-', ''), REPLACE(parent_snapshot_id, '-', ''), + downloaded_at, depth, fs_version, + COALESCE(config, '{}'), COALESCE(notes, ''), + num_uses_succeeded, num_uses_failed, + status, retry_at, current_step + FROM core_snapshot; + + DROP TABLE core_snapshot; + ALTER TABLE core_snapshot_final RENAME TO core_snapshot; + + CREATE INDEX core_snapshot_url_idx ON core_snapshot(url); + CREATE INDEX core_snapshot_timestamp_idx ON core_snapshot(timestamp); + CREATE INDEX core_snapshot_bookmarked_at_idx ON core_snapshot(bookmarked_at); + CREATE INDEX core_snapshot_crawl_id_idx ON core_snapshot(crawl_id); + CREATE INDEX core_snapshot_status_idx ON core_snapshot(status); + CREATE INDEX core_snapshot_retry_at_idx ON core_snapshot(retry_at); + CREATE INDEX core_snapshot_created_at_idx ON core_snapshot(created_at); + CREATE UNIQUE INDEX core_snapshot_url_crawl_unique ON core_snapshot(url, crawl_id); + """ + + +def _make_crawl_id_not_null(apps, schema_editor): + # sqlite-only table rebuild. Reuse Django's own RunSQL statement splitting so + # the sqlite behavior is byte-for-byte identical to the original RunSQL op. + if schema_editor.connection.vendor != "sqlite": + return + migrations.RunSQL(sql=_MAKE_CRAWL_ID_NOT_NULL_SQL).database_forwards("core", schema_editor, None, None) + + +def _pg_sync_schema(apps, schema_editor): + # On postgres the raw sqlite rebuilds above are skipped and crawl was only + # added to migration state; resync the real schema to this migration's + # end-state (empty tables). Snapshot is referenced by SnapshotTag and + # ArchiveResult, so those are rebuilt too to restore FK constraints. + from archivebox.misc.db import rebuild_models_from_migration_state + + rebuild_models_from_migration_state(apps, schema_editor, "core", ["Snapshot", "SnapshotTag", "ArchiveResult"]) + + def create_default_crawl_and_assign_snapshots(apps, schema_editor): """ Create a default crawl for migrated snapshots and assign all snapshots without a crawl to it. Uses raw SQL because the app registry isn't fully populated during migrations. """ + # Legacy-data-only, sqlite-specific (PRAGMA + '?' placeholders). On a fresh + # postgres install core_snapshot has no crawl_id column yet and there are no + # unassigned snapshots, so gate before touching any SQL. + if schema_editor.connection.vendor != "sqlite": + return + from django.db import connection import uuid as uuid_lib from datetime import datetime @@ -89,72 +180,10 @@ class Migration(migrations.Migration): ), migrations.SeparateDatabaseAndState( database_operations=[ - # Now make crawl_id NOT NULL - migrations.RunSQL( - sql=""" - -- Rebuild snapshot table with NOT NULL crawl_id - CREATE TABLE core_snapshot_final ( - id TEXT PRIMARY KEY NOT NULL, - created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - modified_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - - url TEXT NOT NULL, - timestamp VARCHAR(32) NOT NULL UNIQUE, - bookmarked_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - - crawl_id TEXT NOT NULL, - parent_snapshot_id TEXT, - - title VARCHAR(512), - downloaded_at DATETIME, - depth INTEGER NOT NULL DEFAULT 0, - fs_version VARCHAR(10) NOT NULL DEFAULT '0.9.0', - - config TEXT NOT NULL DEFAULT '{}', - notes TEXT NOT NULL DEFAULT '', - num_uses_succeeded INTEGER NOT NULL DEFAULT 0, - num_uses_failed INTEGER NOT NULL DEFAULT 0, - - status VARCHAR(15) NOT NULL DEFAULT 'queued', - retry_at DATETIME, - current_step INTEGER NOT NULL DEFAULT 0, - - FOREIGN KEY (crawl_id) REFERENCES crawls_crawl(id) ON DELETE CASCADE, - FOREIGN KEY (parent_snapshot_id) REFERENCES core_snapshot(id) ON DELETE SET NULL - ); - - INSERT INTO core_snapshot_final ( - id, url, timestamp, title, - bookmarked_at, created_at, modified_at, - crawl_id, parent_snapshot_id, - downloaded_at, depth, fs_version, - config, notes, - num_uses_succeeded, num_uses_failed, - status, retry_at, current_step - ) - SELECT - id, url, timestamp, title, - bookmarked_at, created_at, modified_at, - REPLACE(crawl_id, '-', ''), REPLACE(parent_snapshot_id, '-', ''), - downloaded_at, depth, fs_version, - COALESCE(config, '{}'), COALESCE(notes, ''), - num_uses_succeeded, num_uses_failed, - status, retry_at, current_step - FROM core_snapshot; - - DROP TABLE core_snapshot; - ALTER TABLE core_snapshot_final RENAME TO core_snapshot; - - CREATE INDEX core_snapshot_url_idx ON core_snapshot(url); - CREATE INDEX core_snapshot_timestamp_idx ON core_snapshot(timestamp); - CREATE INDEX core_snapshot_bookmarked_at_idx ON core_snapshot(bookmarked_at); - CREATE INDEX core_snapshot_crawl_id_idx ON core_snapshot(crawl_id); - CREATE INDEX core_snapshot_status_idx ON core_snapshot(status); - CREATE INDEX core_snapshot_retry_at_idx ON core_snapshot(retry_at); - CREATE INDEX core_snapshot_created_at_idx ON core_snapshot(created_at); - CREATE UNIQUE INDEX core_snapshot_url_crawl_unique ON core_snapshot(url, crawl_id); - """, - reverse_sql=migrations.RunSQL.noop, + # Now make crawl_id NOT NULL (sqlite-only table rebuild) + migrations.RunPython( + _make_crawl_id_not_null, + reverse_code=migrations.RunPython.noop, ), ], state_operations=[ @@ -169,4 +198,5 @@ class Migration(migrations.Migration): ), ], ), + migrations.RunPython(_pg_sync_schema, reverse_code=migrations.RunPython.noop), ] diff --git a/archivebox/core/migrations/0025_alter_archiveresult_options_alter_snapshot_options_and_more.py b/archivebox/core/migrations/0025_alter_archiveresult_options_alter_snapshot_options_and_more.py index c4eb2ee8..46d637ad 100644 --- a/archivebox/core/migrations/0025_alter_archiveresult_options_alter_snapshot_options_and_more.py +++ b/archivebox/core/migrations/0025_alter_archiveresult_options_alter_snapshot_options_and_more.py @@ -11,6 +11,12 @@ from archivebox.uuid_compat import uuid7 def copy_old_fields_to_new(apps, schema_editor): """Copy data from old field names to new field names after AddField operations.""" + # sqlite-only legacy data copy (PRAGMA introspection). On postgres there is + # no legacy data and the state-only AddFields below are resynced by + # _pg_sync_schema at the end of this migration. + if schema_editor.connection.vendor != "sqlite": + return + cursor = connection.cursor() # Check if old fields still exist @@ -43,6 +49,19 @@ def copy_old_fields_to_new(apps, schema_editor): # NOTE: UUIDs are already populated by migration 0023 for all migration paths. +def _pg_sync_schema(apps, schema_editor): + # This migration mixes real ORM AddFields with state-only AddFields (the + # snapshot config/current_step/depth/notes/num_uses_*/parent_snapshot/ + # retry_at/status columns and archiveresult created_at/modified_at were + # created on sqlite by 0023's raw rebuild, so here they are state-only). + # On postgres those columns would never land, desyncing the real schema. + # Resync to this migration's final state (empty tables on pg). Snapshot is + # referenced by SnapshotTag and ArchiveResult, so rebuild those too. + from archivebox.misc.db import rebuild_models_from_migration_state + + rebuild_models_from_migration_state(apps, schema_editor, "core", ["Snapshot", "SnapshotTag", "ArchiveResult"]) + + class Migration(migrations.Migration): dependencies = [ ("core", "0024_assign_default_crawl"), @@ -335,4 +354,5 @@ class Migration(migrations.Migration): model_name="snapshot", constraint=models.UniqueConstraint(fields=("timestamp",), name="unique_timestamp"), ), + migrations.RunPython(_pg_sync_schema, reverse_code=migrations.RunPython.noop), ] diff --git a/archivebox/core/migrations/0027_copy_archiveresult_to_process.py b/archivebox/core/migrations/0027_copy_archiveresult_to_process.py index 0b983425..a564b252 100644 --- a/archivebox/core/migrations/0027_copy_archiveresult_to_process.py +++ b/archivebox/core/migrations/0027_copy_archiveresult_to_process.py @@ -275,6 +275,14 @@ def copy_archiveresult_data_to_process(apps, schema_editor): - failed → exited (exit_code=1) - skipped → exited (exit_code=None) """ + # sqlite-only legacy data copy (PRAGMA introspection, '?' placeholders). + # A postgres install can never contain legacy ArchiveResult cmd/pwd data at + # this point, so there is nothing to copy into machine_process. The + # RemoveField ops below are real (non-state-only) and drop the now-empty + # cmd/pwd/cmd_version columns on both vendors, so no resync is needed. + if schema_editor.connection.vendor != "sqlite": + return + cursor = connection.cursor() # Check if old fields still exist (skip if fresh install or already migrated) diff --git a/archivebox/core/migrations/0029_migrate_archiveresult_to_uuid_pk.py b/archivebox/core/migrations/0029_migrate_archiveresult_to_uuid_pk.py index 64045aad..ddf16ea5 100644 --- a/archivebox/core/migrations/0029_migrate_archiveresult_to_uuid_pk.py +++ b/archivebox/core/migrations/0029_migrate_archiveresult_to_uuid_pk.py @@ -26,6 +26,12 @@ def migrate_archiveresult_id_to_uuid(apps, schema_editor): Result: Clean schema with ONLY id as UUIDField (no old_id, no uuid) """ + # sqlite-only table rebuild (sqlite_master, PRAGMA, table copy/rename). On + # postgres this is unnecessary and invalid: the table is empty and the + # state flip of id -> UUIDField is applied by _pg_sync_schema below. + if schema_editor.connection.vendor != "sqlite": + return + cursor = connection.cursor() # Check if table exists and has data @@ -190,6 +196,16 @@ def migrate_archiveresult_id_to_uuid(apps, schema_editor): print(f" ✓ ArchiveResult UUID primary key migration complete ({row_count} records)") +def _pg_sync_schema(apps, schema_editor): + # On postgres the sqlite table rebuild above is skipped; the id -> UUIDField + # flip and uuid-field removal only reach migration state. Resync the real + # (empty) core_archiveresult table to this migration's end-state. Nothing in + # core references ArchiveResult, so no other models need rebuilding. + from archivebox.misc.db import rebuild_models_from_migration_state + + rebuild_models_from_migration_state(apps, schema_editor, "core", ["ArchiveResult"]) + + class Migration(migrations.Migration): dependencies = [ ("core", "0028_alter_snapshot_fs_version"), @@ -217,4 +233,5 @@ class Migration(migrations.Migration): ), ], ), + migrations.RunPython(_pg_sync_schema, reverse_code=migrations.RunPython.noop), ] diff --git a/archivebox/core/migrations/0046_repair_snapshot_permissions.py b/archivebox/core/migrations/0046_repair_snapshot_permissions.py index 53703c90..0317453a 100644 --- a/archivebox/core/migrations/0046_repair_snapshot_permissions.py +++ b/archivebox/core/migrations/0046_repair_snapshot_permissions.py @@ -13,6 +13,13 @@ def _repair_snapshot_permissions(apps, schema_editor): no underlying column. Fresh installs added the column via 0041 and this is a no-op. """ + # sqlite-only legacy repair (PRAGMA table_xinfo + sqlite VIRTUAL generated + # column syntax). On postgres the permissions column was created by 0041's + # portable GeneratedField AddField (STORED generated column), so there is + # nothing to repair. + if schema_editor.connection.vendor != "sqlite": + return + cursor = schema_editor.connection.cursor() # ``table_xinfo`` lists STORED/VIRTUAL generated columns; ``table_info`` # silently drops them, so a prior 0041 that landed the STORED column diff --git a/archivebox/core/migrations/0051_postgres_url_pattern_ops_index.py b/archivebox/core/migrations/0051_postgres_url_pattern_ops_index.py new file mode 100644 index 00000000..ad31ff3b --- /dev/null +++ b/archivebox/core/migrations/0051_postgres_url_pattern_ops_index.py @@ -0,0 +1,30 @@ +# Postgres-only: btree pattern-ops index on core_snapshot.url so LIKE 'prefix%' +# queries (url__startswith and the URL prefix search) stay index scans under any +# database collation. SQLite needs nothing here: its plain url index already +# serves the bytewise range comparisons used on that backend. + +from django.db import migrations + + +def add_pg_url_pattern_index(apps, schema_editor): + if schema_editor.connection.vendor != "postgresql": + return + schema_editor.execute( + "CREATE INDEX IF NOT EXISTS core_snapshot_url_pattern_ops_idx ON core_snapshot (url text_pattern_ops)", + ) + + +def remove_pg_url_pattern_index(apps, schema_editor): + if schema_editor.connection.vendor != "postgresql": + return + schema_editor.execute("DROP INDEX IF EXISTS core_snapshot_url_pattern_ops_idx") + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0050_snapshot_permissions_not_null"), + ] + + operations = [ + migrations.RunPython(add_pg_url_pattern_index, remove_pg_url_pattern_index), + ] diff --git a/archivebox/core/settings.py b/archivebox/core/settings.py index f3e0370f..b278ee2a 100644 --- a/archivebox/core/settings.py +++ b/archivebox/core/settings.py @@ -212,44 +212,18 @@ TEMPLATES = [ ### External Service Settings ################################################################################ +# All sqlite-vs-postgres connection logic lives in archivebox.misc.db; +# DATABASE_ENGINE config selects the backend (sqlite by default). +from archivebox.misc.db import get_database_settings, get_sqlite_connection_options + DATABASE_NAME = CONFIG.DATABASE_NAME SQLITE_JOURNAL_MODE = CONFIG.SQLITE_JOURNAL_MODE SQLITE_MMAP_SIZE = CONFIG.SQLITE_MMAP_SIZE -SQLITE_CONNECTION_OPTIONS = { - "ENGINE": "archivebox.core.sqlite_backend", - "TIME_ZONE": CONSTANTS.TIMEZONE, - "OPTIONS": { - # https://gcollazo.com/optimal-sqlite-settings-for-django/ - # https://litestream.io/tips/#busy-timeout - # https://docs.djangoproject.com/en/5.1/ref/databases/#setting-pragma-options - "timeout": CONFIG.SQLITE_BUSY_TIMEOUT / 1000, - "check_same_thread": False, - # Keep SQLite on Django's default deferred transaction mode. BEGIN - # IMMEDIATE grabs the write lock as soon as atomic() opens, which is - # exactly what hurts ArchiveBox on large collections where Python code - # may do filesystem work before the actual row write. Deferred BEGIN - # keeps writes statement-scoped unless a caller explicitly opens a - # transaction around multiple writes. - "transaction_mode": None, - "init_command": ( - "PRAGMA foreign_keys=ON;" - f"PRAGMA busy_timeout = {CONFIG.SQLITE_BUSY_TIMEOUT};" - f"PRAGMA journal_mode = {SQLITE_JOURNAL_MODE};" - "PRAGMA synchronous = NORMAL;" - "PRAGMA temp_store = MEMORY;" - f"PRAGMA mmap_size = {SQLITE_MMAP_SIZE};" - "PRAGMA journal_size_limit = 67108864;" - "PRAGMA cache_size = 2000;" - ), - }, -} +SQLITE_CONNECTION_OPTIONS = get_sqlite_connection_options() DATABASES = { - "default": { - "NAME": DATABASE_NAME, - **SQLITE_CONNECTION_OPTIONS, - }, + "default": get_database_settings(), } MIGRATION_MODULES = {"signal_webhooks": None} diff --git a/archivebox/core/views.py b/archivebox/core/views.py index d9f4bfe9..c046354c 100644 --- a/archivebox/core/views.py +++ b/archivebox/core/views.py @@ -16,6 +16,7 @@ from django.utils.safestring import mark_safe from django.views import View from django.views.generic.list import ListView from django.views.generic import FormView +from django.db import connection from django.db.models import Case, IntegerField, Q, Value, When from django.core.paginator import InvalidPage from django.contrib import messages @@ -276,12 +277,18 @@ class SnapshotView(View): """ def _fragmentless_url_query(url: str) -> Q: - # Use a range comparison (url >= 'canonical#' AND url < 'canonical#\U0010ffff') - # instead of LIKE/__startswith — SQLite's case-insensitive LIKE bypasses the - # url index and forces a full-table scan over ~1M rows (~250ms). The range - # form lets SQLite use a MULTI-INDEX OR and stays under 1ms. canonical = without_fragment(url) - return Q(url=canonical) | (Q(url__gte=f"{canonical}#") & Q(url__lt=f"{canonical}#\U0010ffff")) + if connection.vendor == "sqlite": + # Use a range comparison (url >= 'canonical#' AND url < 'canonical#\U0010ffff') + # instead of LIKE/__startswith — SQLite's case-insensitive LIKE bypasses the + # url index and forces a full-table scan over ~1M rows (~250ms). The range + # form lets SQLite use a MULTI-INDEX OR and stays under 1ms. + return Q(url=canonical) | (Q(url__gte=f"{canonical}#") & Q(url__lt=f"{canonical}#\U0010ffff")) + # On postgres the range trick is unsafe: linguistic (ICU/libc) collations + # don't compare '#'-suffixed strings bytewise, so the range can miss rows. + # startswith compiles to LIKE 'prefix%' with wildcards escaped, which is + # correct under any collation and uses the url pattern-ops index. + return Q(url=canonical) | Q(url__startswith=f"{canonical}#") normalized = without_fragment(path) if path.startswith(("http://", "https://")): diff --git a/archivebox/crawls/migrations/0001_initial.py b/archivebox/crawls/migrations/0001_initial.py index c90b52ad..6b611a3b 100644 --- a/archivebox/crawls/migrations/0001_initial.py +++ b/archivebox/crawls/migrations/0001_initial.py @@ -10,19 +10,9 @@ from archivebox.uuid_compat import uuid7 from archivebox.base_models.models import get_or_create_system_user_pk -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ("auth", "0012_alter_user_first_name_max_length"), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.SeparateDatabaseAndState( - database_operations=[ - migrations.RunSQL( - sql=""" +# Raw sqlite DDL kept EXACTLY as-is; executed only on sqlite via Django's +# RunSQL (identical statement splitting → byte-for-byte identical behavior). +INITIAL_SQL = """ -- Create crawls_crawlschedule table first (circular FK will be added later) CREATE TABLE IF NOT EXISTS crawls_crawlschedule ( id TEXT PRIMARY KEY NOT NULL, @@ -75,12 +65,48 @@ class Migration(migrations.Migration): CREATE INDEX IF NOT EXISTS crawls_crawl_created_at_idx ON crawls_crawl(created_at); CREATE INDEX IF NOT EXISTS crawls_crawl_created_by_id_idx ON crawls_crawl(created_by_id); CREATE INDEX IF NOT EXISTS crawls_crawl_schedule_id_idx ON crawls_crawl(schedule_id); - """, - reverse_sql=""" + """ + +INITIAL_REVERSE_SQL = """ DROP TABLE IF EXISTS crawls_crawl; DROP TABLE IF EXISTS crawls_crawlschedule; - """, - ), + """ + + +def _run_sqlite_only_sql(apps, schema_editor): + if schema_editor.connection.vendor != "sqlite": + return + migrations.RunSQL(sql=INITIAL_SQL, reverse_sql=INITIAL_REVERSE_SQL).database_forwards( + "crawls", schema_editor, None, None, + ) + + +def _run_sqlite_only_sql_reverse(apps, schema_editor): + if schema_editor.connection.vendor != "sqlite": + return + migrations.RunSQL(sql=INITIAL_SQL, reverse_sql=INITIAL_REVERSE_SQL).database_backwards( + "crawls", schema_editor, None, None, + ) + + +def _pg_sync_schema(apps, schema_editor): + from archivebox.misc.db import rebuild_models_from_migration_state + + rebuild_models_from_migration_state(apps, schema_editor, "crawls", ["CrawlSchedule", "Crawl"]) + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.RunPython(_run_sqlite_only_sql, reverse_code=_run_sqlite_only_sql_reverse), ], state_operations=[ migrations.CreateModel( @@ -174,4 +200,9 @@ class Migration(migrations.Migration): ), ], ), + # On non-sqlite backends the raw DDL above is skipped, so the real + # schema diverges from Django state. Rebuild both tables from the + # post-migration state (no-op on sqlite). Runs as a top-level op so + # ``apps`` reflects the full state including the circular template FK. + migrations.RunPython(_pg_sync_schema, reverse_code=migrations.RunPython.noop), ] diff --git a/archivebox/crawls/migrations/0002_upgrade_from_0_8_6.py b/archivebox/crawls/migrations/0002_upgrade_from_0_8_6.py index eee69b1a..86e86763 100644 --- a/archivebox/crawls/migrations/0002_upgrade_from_0_8_6.py +++ b/archivebox/crawls/migrations/0002_upgrade_from_0_8_6.py @@ -6,6 +6,10 @@ from django.db import migrations, connection def upgrade_crawl_table_from_v086(apps, schema_editor): """Upgrade crawls_crawl table from v0.8.6rc0 schema to v0.9.0 schema.""" + # sqlite-only legacy repair (sqlite_master/PRAGMA/GLOB). Postgres support + # postdates all legacy data, so this never has work to do there. + if schema_editor.connection.vendor != "sqlite": + return cursor = connection.cursor() # Check if crawls_crawl table exists diff --git a/archivebox/crawls/migrations/0016_hydrate_crawl_permissions.py b/archivebox/crawls/migrations/0016_hydrate_crawl_permissions.py index 1377faac..87a7403f 100644 --- a/archivebox/crawls/migrations/0016_hydrate_crawl_permissions.py +++ b/archivebox/crawls/migrations/0016_hydrate_crawl_permissions.py @@ -97,6 +97,12 @@ def _ensure_permissions_column(cursor): def hydrate_crawl_permissions(apps, schema_editor): + # sqlite-only legacy hydration + generated-column repair (PRAGMA + # table_xinfo / VIRTUAL generated column). On postgres the ``permissions`` + # column is created portably by 0013 (AddField GeneratedField) and there is + # never legacy data to hydrate, so this is a no-op there. + if schema_editor.connection.vendor != "sqlite": + return Crawl = apps.get_model("crawls", "Crawl") base_config = raw_base_config(apps) default_permissions = resolve_permissions(base_config, "public") diff --git a/archivebox/machine/migrations/0001_initial.py b/archivebox/machine/migrations/0001_initial.py index 2a0f018c..9425f761 100644 --- a/archivebox/machine/migrations/0001_initial.py +++ b/archivebox/machine/migrations/0001_initial.py @@ -7,16 +7,36 @@ import django.utils.timezone from archivebox.uuid_compat import uuid7 -class Migration(migrations.Migration): - initial = True +def _run_sqlite_initial_ddl(apps, schema_editor): + # sqlite-only: raw DDL creates the real tables byte-for-byte as historically + # shipped. On other vendors the tables are built from migration state by + # _pg_sync_schema below (a fresh non-sqlite DB never holds legacy data here). + if schema_editor.connection.vendor != "sqlite": + return + migrations.RunSQL( + sql=_SQLITE_INITIAL_SQL, + reverse_sql=_SQLITE_INITIAL_REVERSE_SQL, + ).database_forwards("machine", schema_editor, None, None) - dependencies = [] - operations = [ - migrations.SeparateDatabaseAndState( - database_operations=[ - migrations.RunSQL( - sql=""" +def _reverse_sqlite_initial_ddl(apps, schema_editor): + if schema_editor.connection.vendor != "sqlite": + return + migrations.RunSQL( + sql=_SQLITE_INITIAL_SQL, + reverse_sql=_SQLITE_INITIAL_REVERSE_SQL, + ).database_backwards("machine", schema_editor, None, None) + + +def _pg_sync_schema(apps, schema_editor): + from archivebox.misc.db import rebuild_models_from_migration_state + + rebuild_models_from_migration_state( + apps, schema_editor, "machine", ["Machine", "NetworkInterface", "Binary"] + ) + + +_SQLITE_INITIAL_SQL = """ -- Create machine_machine table CREATE TABLE IF NOT EXISTS machine_machine ( id TEXT PRIMARY KEY NOT NULL, @@ -98,13 +118,25 @@ class Migration(migrations.Migration): CREATE INDEX IF NOT EXISTS machine_binary_status_idx ON machine_binary(status); CREATE INDEX IF NOT EXISTS machine_binary_retry_at_idx ON machine_binary(retry_at); - """, - reverse_sql=""" + """ + + +_SQLITE_INITIAL_REVERSE_SQL = """ DROP TABLE IF EXISTS machine_binary; DROP TABLE IF EXISTS machine_networkinterface; DROP TABLE IF EXISTS machine_machine; - """, - ), + """ + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.RunPython(_run_sqlite_initial_ddl, _reverse_sqlite_initial_ddl), ], state_operations=[ migrations.CreateModel( @@ -244,4 +276,5 @@ class Migration(migrations.Migration): ), ], ), + migrations.RunPython(_pg_sync_schema, reverse_code=migrations.RunPython.noop), ] diff --git a/archivebox/machine/migrations/0005_converge_binary_model.py b/archivebox/machine/migrations/0005_converge_binary_model.py index c1e89175..cbbac9b2 100644 --- a/archivebox/machine/migrations/0005_converge_binary_model.py +++ b/archivebox/machine/migrations/0005_converge_binary_model.py @@ -10,6 +10,13 @@ def converge_binary_table(apps, schema_editor): Drop machine_installedbinary if it exists (0.8.6rc0 path). Create machine_binary if it doesn't exist (needed by Process model). """ + # sqlite-only legacy convergence: on a fresh install machine_binary already + # exists with the final column set (created by 0001), so every branch below + # is a no-op schema-wise. A non-sqlite DB never carries the 0.8.x legacy + # shapes this repairs, and its Binary table already matches migration state. + if schema_editor.connection.vendor != "sqlite": + return + cursor = connection.cursor() # Check what tables exist diff --git a/archivebox/machine/migrations/0011_remove_binary_output_dir.py b/archivebox/machine/migrations/0011_remove_binary_output_dir.py index 0a24dff1..92f0e935 100644 --- a/archivebox/machine/migrations/0011_remove_binary_output_dir.py +++ b/archivebox/machine/migrations/0011_remove_binary_output_dir.py @@ -2,9 +2,17 @@ from django.db import migrations def remove_output_dir_if_exists(apps, schema_editor): - cursor = schema_editor.connection.cursor() - cursor.execute("PRAGMA table_info(machine_binary)") - columns = {row[1] for row in cursor.fetchall()} + if schema_editor.connection.vendor == "sqlite": + cursor = schema_editor.connection.cursor() + cursor.execute("PRAGMA table_info(machine_binary)") + columns = {row[1] for row in cursor.fetchall()} + else: + # Portable introspection: on non-sqlite the RemoveField below is + # state-only, so the real ALTER TABLE DROP COLUMN must run here too. + # A fresh non-sqlite DB has machine_binary with output_dir from 0001. + from archivebox.misc.db import migration_table_columns + + columns = migration_table_columns(schema_editor.connection, "machine_binary") if "output_dir" not in columns: return diff --git a/archivebox/machine/migrations/0012_add_machine_config_if_missing.py b/archivebox/machine/migrations/0012_add_machine_config_if_missing.py index d462b8ce..e268b69e 100644 --- a/archivebox/machine/migrations/0012_add_machine_config_if_missing.py +++ b/archivebox/machine/migrations/0012_add_machine_config_if_missing.py @@ -2,6 +2,12 @@ from django.db import migrations def add_machine_config_if_missing(apps, schema_editor): + # sqlite-only: uses PRAGMA introspection. On a fresh non-sqlite install the + # config column already exists (created from 0001 migration state), so there + # is nothing to add. + if schema_editor.connection.vendor != "sqlite": + return + cursor = schema_editor.connection.cursor() cursor.execute("PRAGMA table_info(machine_machine)") columns = {row[1] for row in cursor.fetchall()} diff --git a/archivebox/misc/db.py b/archivebox/misc/db.py index a154d16b..746dcd57 100644 --- a/archivebox/misc/db.py +++ b/archivebox/misc/db.py @@ -12,6 +12,7 @@ from pathlib import Path from typing import TextIO from typing import Any import fcntl +import os import time from collections.abc import Callable from contextlib import contextmanager @@ -21,6 +22,267 @@ from archivebox.config import CONSTANTS from archivebox.misc.util import enforce_types +# ============================================================================ +# Database backend adapter (sqlite / postgresql) +# ============================================================================ +# All sqlite-vs-postgres branching in ArchiveBox is centralized in this +# section. Code elsewhere should call these helpers instead of checking +# ``connection.vendor``, building ``DATABASES`` entries, or touching +# ``CONSTANTS.DATABASE_FILE`` directly. + +SQLITE_VENDOR = "sqlite" +POSTGRES_VENDOR = "postgresql" + + +def database_backend() -> str: + """Configured backend vendor name: 'sqlite' (default) or 'postgresql'.""" + from archivebox.config.common import get_config + + engine = (get_config().DATABASE_ENGINE or "sqlite").strip().lower() + return POSTGRES_VENDOR if engine.startswith("postgres") else SQLITE_VENDOR + + +def is_postgres() -> bool: + return database_backend() == POSTGRES_VENDOR + + +def postgres_db_params() -> dict[str, str]: + """Postgres connection params from config (NAME/USER/PASSWORD/HOST/PORT).""" + from archivebox.config.common import get_config + + config = get_config() + name = config.DATABASE_NAME + # DATABASE_NAME defaults to the sqlite file path; that default makes no + # sense as a postgres database name, so fall back to 'archivebox'. + if name == str(CONSTANTS.DATABASE_FILE) or name.endswith(".sqlite3"): + name = "archivebox" + return { + "NAME": name, + "USER": config.DATABASE_USER, + "PASSWORD": config.DATABASE_PASSWORD, + "HOST": config.DATABASE_HOST, + "PORT": str(config.DATABASE_PORT), + } + + +def get_sqlite_connection_options() -> dict[str, Any]: + """ENGINE + OPTIONS for the sqlite backend (without NAME).""" + from archivebox.config.common import get_config + + config = get_config() + return { + "ENGINE": "archivebox.core.sqlite_backend", + "TIME_ZONE": CONSTANTS.TIMEZONE, + "OPTIONS": { + # https://gcollazo.com/optimal-sqlite-settings-for-django/ + # https://litestream.io/tips/#busy-timeout + # https://docs.djangoproject.com/en/5.1/ref/databases/#setting-pragma-options + "timeout": config.SQLITE_BUSY_TIMEOUT / 1000, + "check_same_thread": False, + # Keep SQLite on Django's default deferred transaction mode. BEGIN + # IMMEDIATE grabs the write lock as soon as atomic() opens, which is + # exactly what hurts ArchiveBox on large collections where Python code + # may do filesystem work before the actual row write. Deferred BEGIN + # keeps writes statement-scoped unless a caller explicitly opens a + # transaction around multiple writes. + "transaction_mode": None, + "init_command": ( + "PRAGMA foreign_keys=ON;" + f"PRAGMA busy_timeout = {config.SQLITE_BUSY_TIMEOUT};" + f"PRAGMA journal_mode = {config.SQLITE_JOURNAL_MODE};" + "PRAGMA synchronous = NORMAL;" + "PRAGMA temp_store = MEMORY;" + f"PRAGMA mmap_size = {config.SQLITE_MMAP_SIZE};" + "PRAGMA journal_size_limit = 67108864;" + "PRAGMA cache_size = 2000;" + ), + }, + } + + +def get_database_settings() -> dict[str, Any]: + """The DATABASES['default'] dict for Django settings, per configured backend.""" + from archivebox.config.common import get_config + + if is_postgres(): + return { + "ENGINE": "django.db.backends.postgresql", + **postgres_db_params(), + "OPTIONS": {"connect_timeout": 10}, + } + return { + "NAME": get_config().DATABASE_NAME, + **get_sqlite_connection_options(), + } + + +def _psycopg_connect(dbname: str | None = None, connect_timeout: int = 5): + import psycopg + + params = postgres_db_params() + return psycopg.connect( + dbname=dbname or params["NAME"], + user=params["USER"], + password=params["PASSWORD"] or None, + host=params["HOST"], + port=params["PORT"], + connect_timeout=connect_timeout, + ) + + +def database_exists() -> bool: + """True if this collection's database has been initialized. + + sqlite: the index.sqlite3 file exists on disk. + postgres: the configured database is reachable and contains the + django_migrations table. Safe to call before Django is set up. + """ + if not is_postgres(): + return os.path.isfile(CONSTANTS.DATABASE_FILE) + try: + with _psycopg_connect() as conn: + row = conn.execute("SELECT to_regclass('django_migrations')").fetchone() + return bool(row and row[0]) + except Exception: + return False + + +def database_display_location() -> str: + """Human-readable location of the database (file path or postgres DSN).""" + if not is_postgres(): + return str(CONSTANTS.DATABASE_FILE) + params = postgres_db_params() + return f"postgresql://{params['USER']}@{params['HOST']}:{params['PORT']}/{params['NAME']}" + + +def ensure_database_ready() -> None: + """Make sure a database server is reachable before running migrations. + + sqlite: no-op (the file is created on first connection). + postgres: verify the server accepts connections and create the configured + database if it does not exist yet. Raises SystemExit with a helpful + message if the server is unreachable. + """ + if not is_postgres(): + return + + import psycopg + from rich import print as rich_print + + params = postgres_db_params() + try: + with _psycopg_connect(): + return + except psycopg.OperationalError as err: + # 3D000 invalid_catalog_name: server is up but the database is missing + if getattr(err, "sqlstate", None) != "3D000" and "does not exist" not in str(err): + rich_print(f"[red][X] Error: Unable to connect to PostgreSQL at {database_display_location()}[/red]") + rich_print(f" {err}") + rich_print(" [violet]Hint:[/violet] Check ARCHIVEBOX_DATABASE_HOST/PORT/USER/PASSWORD and that the server is running.") + raise SystemExit(4) from err + + with _psycopg_connect(dbname="postgres") as conn: + conn.autocommit = True + safe_name = params["NAME"].replace('"', '""') + conn.execute(f'CREATE DATABASE "{safe_name}"') + rich_print(f" + Created PostgreSQL database {params['NAME']}") + + +def is_missing_table_error(err: BaseException) -> bool: + """True if err means a queried table does not exist (any backend).""" + msg = str(err).lower() + return "no such table" in msg or ("relation" in msg and "does not exist" in msg) + + +def approximate_row_counts(connection) -> dict[str, int]: + """Cheap per-table approximate row counts from the backend's optimizer stats. + + sqlite: reads sqlite_stat1 (populated by ANALYZE). + postgres: reads pg_class.reltuples (maintained by autovacuum/ANALYZE). + Returns {} on any failure; tables never analyzed may be absent. + """ + counts: dict[str, int] = {} + try: + with connection.cursor() as cursor: + if connection.vendor == SQLITE_VENDOR: + cursor.execute("SELECT tbl, stat FROM sqlite_stat1") + for table, stat in cursor.fetchall(): + try: + counts[str(table)] = int(str(stat).split()[0]) + except (IndexError, TypeError, ValueError): + continue + elif connection.vendor == POSTGRES_VENDOR: + cursor.execute( + """ + SELECT c.relname, c.reltuples::bigint + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind = 'r' + AND n.nspname = current_schema() + AND c.reltuples >= 0 + """, + ) + counts = {str(table): int(estimate) for table, estimate in cursor.fetchall()} + except Exception: + return {} + return counts + + +def truncate_overlong_charfields(sender, instance, **kwargs) -> None: + """pre_save receiver: clamp CharField values to their declared max_length. + + SQLite never enforces VARCHAR(n) limits, so ArchiveBox has always stored + overlong values (e.g. long crawl labels or page titles) untruncated. + PostgreSQL enforces them and would raise DataError on save instead. + Truncating at save time keeps writes succeeding identically on both + backends. Registered once for all models in CoreConfig.ready(). + """ + from django.db import models as dj_models + + for field in instance._meta.local_concrete_fields: + if isinstance(field, dj_models.CharField) and field.max_length: + value = getattr(instance, field.attname, None) + if isinstance(value, str) and len(value) > field.max_length: + setattr(instance, field.attname, value[: field.max_length]) + + +# --- migration helpers ------------------------------------------------------ + + +def migration_table_exists(connection, table_name: str) -> bool: + """Portable existence check usable from inside migrations.""" + return table_name in connection.introspection.table_names() + + +def migration_table_columns(connection, table_name: str) -> set[str]: + """Portable column-name introspection usable from inside migrations.""" + with connection.cursor() as cursor: + return {col.name for col in connection.introspection.get_table_description(cursor, table_name)} + + +def rebuild_models_from_migration_state(apps, schema_editor, app_label: str, model_names: list[str]) -> None: + """(non-sqlite only) Drop and recreate the given models' tables from the + current migration state. + + ArchiveBox's historical sqlite migrations rebuild tables with raw SQL that + intentionally diverges from Django migration state (state-only AddFields + reconciled by later sqlite rebuilds). Postgres support postdates all of + them, so a non-sqlite database can never contain legacy data at these + points in history: every affected table is empty, and dropping + recreating + it from state is always equivalent, keeping the real schema in lockstep + with migration state at each divergence point. No-op on sqlite. + """ + if schema_editor.connection.vendor == SQLITE_VENDOR: + return + existing_tables = set(schema_editor.connection.introspection.table_names()) + models = [apps.get_model(app_label, model_name) for model_name in model_names] + for model in models: + if model._meta.db_table in existing_tables: + schema_editor.delete_model(model) + for model in models: + schema_editor.create_model(model) + + def run_db_analyze_batch( remaining: list[str] | None, *, @@ -357,7 +619,7 @@ def migration_state(out_dir: Path = CONSTANTS.DATA_DIR) -> tuple[list[str], list try: cursor.execute("SELECT app, name FROM django_migrations") except Exception as err: - if "no such table" in str(err).lower(): + if is_missing_table_error(err): return set() raise return {(str(app), str(name)) for app, name in cursor.fetchall()} diff --git a/archivebox/misc/logging_util.py b/archivebox/misc/logging_util.py index a14ac3b5..06170d9e 100644 --- a/archivebox/misc/logging_util.py +++ b/archivebox/misc/logging_util.py @@ -410,7 +410,8 @@ def printable_folder_status(name: str, folder: dict) -> str: else: color, symbol, note, num_files = "grey53", "-", "unused", "-" - if folder["path"]: + if folder["path"] and "://" not in str(folder["path"]): + # file-count probing only makes sense for filesystem paths, not DSNs if os.access(folder["path"], os.R_OK): try: num_files = ( diff --git a/archivebox/personas/migrations/0004_hydrate_persona_permissions.py b/archivebox/personas/migrations/0004_hydrate_persona_permissions.py index 7b7bd22e..ec4a0cd8 100644 --- a/archivebox/personas/migrations/0004_hydrate_persona_permissions.py +++ b/archivebox/personas/migrations/0004_hydrate_persona_permissions.py @@ -92,6 +92,12 @@ def _ensure_permissions_column(cursor): def hydrate_persona_permissions(apps, schema_editor): + # sqlite-only legacy hydration + generated-column repair (PRAGMA + # table_xinfo / VIRTUAL generated column). On postgres the ``permissions`` + # column is created portably by 0003 (AddField GeneratedField) and there is + # never legacy data to hydrate, so this is a no-op there. + if schema_editor.connection.vendor != "sqlite": + return Persona = apps.get_model("personas", "Persona") base_config = raw_base_config(apps) default_permissions = resolve_permissions(base_config, "public") diff --git a/archivebox/search/query.py b/archivebox/search/query.py index 2e01537d..2142faa4 100644 --- a/archivebox/search/query.py +++ b/archivebox/search/query.py @@ -16,31 +16,38 @@ MAX_SEARCH_RANK_IDS = 500 def escape_like_query(query: str) -> str: - """Escape a string for SQLite LIKE matching.""" + """Escape a string for SQL LIKE matching (used with ESCAPE '\\').""" return query.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") def crawl_config_values_search_wave(query: str) -> Q | None: """Build a Snapshot Q predicate matching values inside Crawl.config.""" - if connection.vendor != "sqlite": - return None - from archivebox.crawls.models import Crawl pattern = f"%{escape_like_query(query).lower()}%" - matching_crawls = Crawl.objects.extra( - where=[ - """ - EXISTS ( - SELECT 1 - FROM json_tree(config) - WHERE json_tree.atom IS NOT NULL - AND LOWER(CAST(json_tree.atom AS TEXT)) LIKE %s ESCAPE '\\' - ) - """, - ], - params=[pattern], - ) + if connection.vendor == "sqlite": + matching_crawls = Crawl.objects.extra( + where=[ + """ + EXISTS ( + SELECT 1 + FROM json_tree(config) + WHERE json_tree.atom IS NOT NULL + AND LOWER(CAST(json_tree.atom AS TEXT)) LIKE %s ESCAPE '\\' + ) + """, + ], + params=[pattern], + ) + elif connection.vendor == "postgresql": + # jsonb has no json_tree() equivalent; matching the serialized jsonb text + # covers every nested value (keys can also match, which only widens recall). + matching_crawls = Crawl.objects.extra( + where=["LOWER(config::text) LIKE %s ESCAPE '\\'"], + params=[pattern], + ) + else: + return None return Q(crawl_id__in=matching_crawls.values("pk")) diff --git a/archivebox/search/views.py b/archivebox/search/views.py index 8d1f2799..4bbd9220 100644 --- a/archivebox/search/views.py +++ b/archivebox/search/views.py @@ -124,19 +124,32 @@ def iter_url_prefix_search_ids(prefix: str, queryset): table = connection.ops.quote_name(model._meta.db_table) pk_column = connection.ops.quote_name(model._meta.pk.column) url_column = connection.ops.quote_name(model._meta.get_field("url").column) - upper_bound = url_prefix_upper_bound(prefix) raw_ids = [] + if connection.vendor == "sqlite": + # Bytewise range comparison uses the plain url btree index directly. + where_clause = f"{url_column} >= %s AND {url_column} < %s" + where_params = [prefix, url_prefix_upper_bound(prefix)] + else: + # Range comparisons are collation-dependent on postgres (linguistic + # collations don't compare bytewise), so use LIKE with escaped + # wildcards instead — correct under any collation and able to use the + # url pattern-ops index. + from archivebox.search.query import escape_like_query + + where_clause = f"{url_column} LIKE %s ESCAPE '\\'" + where_params = [f"{escape_like_query(prefix)}%"] + with connection.cursor() as cursor: cursor.execute( f""" SELECT {pk_column} FROM {table} - WHERE {url_column} >= %s AND {url_column} < %s + WHERE {where_clause} ORDER BY {url_column} LIMIT %s """, - [prefix, upper_bound, URL_PREFIX_SEARCH_LIMIT], + [*where_params, URL_PREFIX_SEARCH_LIMIT], ) raw_ids = [str(row[0]).replace("-", "") for row in cursor.fetchall()] diff --git a/archivebox/tests/test_postgres_backend.py b/archivebox/tests/test_postgres_backend.py new file mode 100644 index 00000000..29adccd9 --- /dev/null +++ b/archivebox/tests/test_postgres_backend.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +""" +End-to-end tests for the PostgreSQL database backend (DATABASE_ENGINE=postgres). + +Spins up a real throwaway PostgreSQL cluster (initdb + pg_ctl) for the module, +then exercises real archivebox CLI flows against it: init, status, re-init, +add --index-only, list, remove, and a full schema-vs-models parity check. + +Requires PostgreSQL server binaries (initdb/pg_ctl) to be installed. +""" + +import os +import shlex +import shutil +import socket +import subprocess +import tempfile +import uuid +from pathlib import Path + +import pytest + +from .conftest import cli_env, run_archivebox_cmd, run_queued_crawls + + +def _find_pg_bindir() -> Path: + """Locate PostgreSQL server binaries (initdb) on this machine.""" + initdb_on_path = shutil.which("initdb") + if initdb_on_path: + return Path(initdb_on_path).resolve().parent + candidates = [] + for base in (Path("/usr/lib/postgresql"), Path("/opt/homebrew/opt"), Path("/usr/local/opt"), Path("/opt/homebrew/Cellar/postgresql")): + if base.is_dir(): + for sub in sorted(base.iterdir(), reverse=True): + initdb = sub / "bin" / "initdb" + if initdb.is_file(): + candidates.append(initdb.parent) + assert candidates, "PostgreSQL server binaries (initdb) are required for test_postgres_backend tests" + return candidates[0] + + +def _free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +PG_TEST_USER = "abxtest" + + +@pytest.fixture(scope="module") +def pg_server(): + """A real throwaway PostgreSQL server for the whole test module.""" + bindir = _find_pg_bindir() + workdir = Path(tempfile.mkdtemp(prefix="abx-pg-test-")) + datadir = workdir / "data" + logfile = workdir / "server.log" + port = _free_port() + + # postgres refuses to run as root; when the suite runs as root (e.g. in a + # dev container), run the server as the system postgres user instead. + run_as_postgres_user = os.geteuid() == 0 + if run_as_postgres_user: + workdir.chmod(0o755) + shutil.chown(workdir, user="postgres") + + def pg_cmd(args: list[str]) -> subprocess.CompletedProcess: + if run_as_postgres_user: + full = ["su", "postgres", "-s", "/bin/sh", "-c", shlex.join(args)] + else: + full = args + return subprocess.run(full, capture_output=True, text=True, check=False, timeout=120) + + result = pg_cmd([str(bindir / "initdb"), "-D", str(datadir), "-E", "UTF8", "-A", "trust", "-U", PG_TEST_USER]) + assert result.returncode == 0, f"initdb failed: {result.stderr}" + + server_opts = f"-p {port} -k {workdir} -c listen_addresses=127.0.0.1 -c fsync=off -c synchronous_commit=off" + result = pg_cmd([str(bindir / "pg_ctl"), "-D", str(datadir), "-l", str(logfile), "-o", server_opts, "-w", "start"]) + assert result.returncode == 0, f"pg_ctl start failed: {result.stderr}\n{logfile.read_text() if logfile.exists() else ''}" + + try: + yield {"host": "127.0.0.1", "port": port, "user": PG_TEST_USER} + finally: + pg_cmd([str(bindir / "pg_ctl"), "-D", str(datadir), "-m", "immediate", "stop"]) + shutil.rmtree(workdir, ignore_errors=True) + + +def pg_cli_env(pg_server: dict, dbname: str, **extra) -> dict: + env = cli_env(**extra) + env.update( + { + "ARCHIVEBOX_DATABASE_ENGINE": "postgres", + "ARCHIVEBOX_DATABASE_HOST": pg_server["host"], + "ARCHIVEBOX_DATABASE_PORT": str(pg_server["port"]), + "ARCHIVEBOX_DATABASE_USER": pg_server["user"], + "ARCHIVEBOX_DATABASE_NAME": dbname, + }, + ) + return env + + +def unique_dbname() -> str: + return f"abx_test_{uuid.uuid4().hex[:12]}" + + +def pg_query(pg_server: dict, dbname: str, query: str) -> list[tuple]: + import psycopg + + with psycopg.connect(host=pg_server["host"], port=pg_server["port"], user=pg_server["user"], dbname=dbname) as conn: + return conn.execute(query).fetchall() + + +def test_init_creates_postgres_schema(pg_server, tmp_path): + """Fresh init against postgres should create the database + full schema, and no sqlite file.""" + dbname = unique_dbname() + env = pg_cli_env(pg_server, dbname) + + result = run_archivebox_cmd(["init", "--quick"], cwd=tmp_path, env=env, timeout=300) + assert result.returncode == 0, f"Init failed: {result.stderr}\n{result.stdout}" + + assert not (tmp_path / "index.sqlite3").exists(), "sqlite file should not be created when using postgres" + assert (tmp_path / "archive").is_dir(), "Archive dir not created" + + (applied_migrations,) = pg_query(pg_server, dbname, "SELECT COUNT(*) FROM django_migrations")[0] + assert applied_migrations > 50, f"Expected all migrations applied, got {applied_migrations}" + + for table in ("core_snapshot", "core_archiveresult", "core_tag", "crawls_crawl", "machine_machine", "api_apitoken", "personas_persona"): + (regclass,) = pg_query(pg_server, dbname, f"SELECT to_regclass('{table}')")[0] + assert regclass == table, f"Table {table} missing from postgres schema" + + +def test_postgres_schema_matches_models(pg_server, tmp_path): + """Every model column must exist in postgres and vice versa (no state/schema drift).""" + dbname = unique_dbname() + env = pg_cli_env(pg_server, dbname) + + result = run_archivebox_cmd(["init", "--quick"], cwd=tmp_path, env=env, timeout=300) + assert result.returncode == 0, f"Init failed: {result.stderr}" + + parity_script = ( + "from django.apps import apps\n" + "from django.db import connection\n" + "problems = []\n" + "with connection.cursor() as cursor:\n" + " tables = set(connection.introspection.table_names(cursor))\n" + " for model in apps.get_models(include_auto_created=True):\n" + " meta = model._meta\n" + " if not meta.managed or meta.proxy:\n" + " continue\n" + " if meta.db_table not in tables:\n" + " problems.append(f'missing table {meta.db_table}')\n" + " continue\n" + " db_cols = {col.name for col in connection.introspection.get_table_description(cursor, meta.db_table)}\n" + " model_cols = {field.column for field in meta.local_concrete_fields}\n" + " for col in sorted(model_cols - db_cols):\n" + " problems.append(f'{meta.db_table}: missing column {col}')\n" + " for col in sorted(db_cols - model_cols):\n" + " problems.append(f'{meta.db_table}: extra column {col}')\n" + "print('SCHEMA_PROBLEMS=' + repr(sorted(problems)))\n" + ) + result = run_archivebox_cmd(["manage", "shell", "-c", parity_script], cwd=tmp_path, env=env, timeout=120) + assert result.returncode == 0, f"manage shell failed: {result.stderr}" + assert "SCHEMA_PROBLEMS=[]" in result.stdout, f"Postgres schema diverges from models:\n{result.stdout}\n{result.stderr}" + + result = run_archivebox_cmd(["manage", "makemigrations", "--check", "--dry-run"], cwd=tmp_path, env=env, timeout=120) + assert result.returncode == 0, f"Model state does not match migrations: {result.stdout}\n{result.stderr}" + + +def test_status_and_reinit_on_postgres(pg_server, tmp_path): + """status works against postgres, and a second init takes the 'verify existing' path.""" + dbname = unique_dbname() + env = pg_cli_env(pg_server, dbname) + + result = run_archivebox_cmd(["init", "--quick"], cwd=tmp_path, env=env, timeout=300) + assert result.returncode == 0, f"Init failed: {result.stderr}" + assert "Initializing a new ArchiveBox" in result.stdout + + result = run_archivebox_cmd(["status"], cwd=tmp_path, env=env, timeout=120) + assert result.returncode == 0, f"Status failed: {result.stderr}" + assert f"postgresql://{PG_TEST_USER}@" in result.stdout, f"status should show the postgres DSN:\n{result.stdout}" + + result = run_archivebox_cmd(["init", "--quick"], cwd=tmp_path, env=env, timeout=300) + assert result.returncode == 0, f"Re-init failed: {result.stderr}" + assert "Verifying and updating existing ArchiveBox collection" in result.stdout + + +def test_add_list_remove_on_postgres(pg_server, tmp_path): + """Real add/list/remove CLI flows store and retrieve rows from postgres.""" + dbname = unique_dbname() + env = pg_cli_env(pg_server, dbname, disable_extractors=True) + test_url = "https://example.com/abx-postgres-test" + + result = run_archivebox_cmd(["init", "--quick"], cwd=tmp_path, env=env, timeout=300) + assert result.returncode == 0, f"Init failed: {result.stderr}" + + result = run_archivebox_cmd(["add", "--index-only", test_url], cwd=tmp_path, env=env, timeout=300) + assert result.returncode == 0, f"Add failed: {result.stderr}\n{result.stdout}" + run_queued_crawls(tmp_path, env) + + (snapshot_count,) = pg_query(pg_server, dbname, "SELECT COUNT(*) FROM core_snapshot")[0] + assert snapshot_count >= 1, "Snapshot row not written to postgres" + (crawl_count,) = pg_query(pg_server, dbname, "SELECT COUNT(*) FROM crawls_crawl")[0] + assert crawl_count >= 1, "Crawl row not written to postgres" + + result = run_archivebox_cmd(["list"], cwd=tmp_path, env=env, timeout=120) + assert result.returncode == 0, f"List failed: {result.stderr}" + assert "example.com/abx-postgres-test" in result.stdout, f"Added URL missing from list output:\n{result.stdout}" + + result = run_archivebox_cmd(["search", "abx-postgres-test"], cwd=tmp_path, env=env, timeout=120) + assert result.returncode == 0, f"Search failed: {result.stderr}" + assert "example.com/abx-postgres-test" in result.stdout, f"Added URL missing from search output:\n{result.stdout}" + + result = run_archivebox_cmd( + ["remove", "--yes", "--delete", "--filter-type=exact", test_url], + cwd=tmp_path, + env=env, + timeout=120, + ) + assert result.returncode == 0, f"Remove failed: {result.stderr}\n{result.stdout}" + (snapshot_count,) = pg_query(pg_server, dbname, f"SELECT COUNT(*) FROM core_snapshot WHERE url = '{test_url}'")[0] + assert snapshot_count == 0, "Snapshot row should be deleted from postgres" + + +def test_sqlite_remains_the_default(tmp_path): + """Without DATABASE_ENGINE config, init keeps using the sqlite file backend.""" + result = run_archivebox_cmd(["init", "--quick"], cwd=tmp_path, env=cli_env(), timeout=300) + assert result.returncode == 0, f"Init failed: {result.stderr}" + assert (tmp_path / "index.sqlite3").exists(), "sqlite file should be created by default" diff --git a/bin/benchmark_db_backends.py b/bin/benchmark_db_backends.py new file mode 100644 index 00000000..5a344f21 --- /dev/null +++ b/bin/benchmark_db_backends.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +"""Benchmark ArchiveBox hot-path index queries at large row counts. + +Run from inside an initialized (empty) collection directory, with the same +ARCHIVEBOX_DATABASE_* env vars the collection was initialized with: + + cd /path/to/collection + uv run --project /path/to/ArchiveBox python /path/to/ArchiveBox/bin/benchmark_db_backends.py --rows 1000000 + +Seeds N snapshots (+1 archiveresult each) via bulk_create, runs ANALYZE, then +times the hot queries used by the admin UI, snapshot detail views, URL prefix +search, and the worker queue/claim paths. Works on both sqlite and postgres — +use it to compare backends or to catch performance regressions. +""" + +import argparse +import json +import os +import statistics +import sys +import time +from datetime import datetime, timedelta, timezone + + +def seed(rows: int, batch_size: int = 20_000) -> None: + from django.db import transaction + + from archivebox.base_models.models import get_or_create_system_user_pk + from archivebox.core.models import ArchiveResult, Snapshot + from archivebox.crawls.models import Crawl + from archivebox.uuid_compat import uuid7 + + user_pk = get_or_create_system_user_pk() + crawl = Crawl.objects.create(urls="https://example.com/", created_by_id=user_pk, status="sealed", label="benchmark seed") + + existing = Snapshot.objects.count() + if existing >= rows: + print(f"already seeded ({existing} snapshots)", flush=True) + return + + base_time = datetime(2020, 1, 1, tzinfo=timezone.utc) + started = time.monotonic() + for start in range(existing, rows, batch_size): + end = min(start + batch_size, rows) + snapshots = [] + for i in range(start, end): + created = base_time + timedelta(seconds=i) + snapshots.append( + Snapshot( + id=uuid7(), + url=f"https://site{i % 1000}.example.org/path/{i}" + ("#section" if i % 7 == 0 else ""), + timestamp=f"{1577836800 + i}.{i}", + title=f"Benchmark page {i}", + crawl=crawl, + bookmarked_at=created, + created_at=created, + modified_at=created, + downloaded_at=created if i % 10 else None, + status="sealed" if i % 20 else "queued", + retry_at=None if i % 20 else created, + fs_version="0.9.0", + config={}, + depth=0, + ), + ) + with transaction.atomic(): + Snapshot.objects.bulk_create(snapshots, batch_size=batch_size) + ArchiveResult.objects.bulk_create( + [ + ArchiveResult( + id=uuid7(), + snapshot=snapshot, + plugin="wget", + hook_name="on_Snapshot__06_wget.py", + status="succeeded", + created_at=snapshot.created_at, + modified_at=snapshot.created_at, + start_ts=snapshot.created_at, + end_ts=snapshot.created_at, + output_str="benchmark", + output_files={}, + ) + for snapshot in snapshots + ], + batch_size=batch_size, + ) + if (end // batch_size) % 5 == 0 or end == rows: + rate = (end - existing) / max(time.monotonic() - started, 0.001) + print(f" seeded {end}/{rows} snapshots ({rate:,.0f} rows/s)", flush=True) + + +def analyze_tables() -> None: + from django.db import connection + + with connection.cursor() as cursor: + for table in ("core_snapshot", "core_archiveresult"): + cursor.execute(f"ANALYZE {table}") + + +def timed(func, repeat: int = 5) -> tuple[float, object]: + times = [] + result = None + for _ in range(repeat): + started = time.perf_counter() + result = func() + times.append((time.perf_counter() - started) * 1000) + return statistics.median(times), result + + +def run_benchmarks(rows: int) -> dict[str, float]: + from django.db import connection + from django.db.models import Count, Q + from django.utils import timezone as dj_timezone + + from archivebox.core.models import ArchiveResult, Snapshot + from archivebox.misc.db import approximate_row_counts + from archivebox.search.views import iter_url_prefix_search_ids + + now = dj_timezone.now() + results: dict[str, float] = {} + target_i = rows // 2 + target_url = f"https://site{target_i % 1000}.example.org/path/{target_i}" + + def fragmentless_q(url: str) -> Q: + if connection.vendor == "sqlite": + return Q(url=url) | (Q(url__gte=f"{url}#") & Q(url__lt=f"{url}#\U0010ffff")) + return Q(url=url) | Q(url__startswith=f"{url}#") + + benchmarks = { + "exact_count": lambda: Snapshot.objects.count(), + "approximate_row_counts": lambda: approximate_row_counts(connection), + "admin_list_page": lambda: list(Snapshot.objects.order_by("-bookmarked_at").values("id", "url", "title", "status", "bookmarked_at")[:40]), + "admin_list_page_offset_10k": lambda: list(Snapshot.objects.order_by("-bookmarked_at").values("id", "url", "title")[10_000:10_040]), + "snapshot_detail_by_url": lambda: list(Snapshot.objects.filter(fragmentless_q(target_url))[:10]), + "snapshot_detail_archiveresults": lambda: list( + ArchiveResult.objects.filter(snapshot__url=target_url).order_by("start_ts").values("id", "plugin", "status")[:100], + ), + "url_prefix_search": lambda: list(iter_url_prefix_search_ids("https://site500.example.org/", Snapshot.objects.all())), + "worker_queue_scan": lambda: list( + Snapshot.objects.filter(status="queued", retry_at__lte=now).order_by("retry_at", "created_at").values_list("id", flat=True)[:100], + ), + "status_facet_counts": lambda: dict(Snapshot.objects.values_list("status").annotate(n=Count("id")).values_list("status", "n")), + "tag_join_filter": lambda: list(Snapshot.objects.filter(title__icontains="page 4242").values("id")[:20]), + } + + for name, func in benchmarks.items(): + median_ms, _ = timed(func) + results[name] = round(median_ms, 2) + print(f" {name:35s} {median_ms:10.2f} ms", flush=True) + + def claim_one() -> int: + snapshot = Snapshot.objects.filter(status="queued").order_by("retry_at").first() + if snapshot is None: + return 0 + return Snapshot.objects.filter(pk=snapshot.pk, retry_at=snapshot.retry_at).update(retry_at=now + timedelta(seconds=60), modified_at=now) + + median_ms, _ = timed(claim_one) + results["worker_cas_claim"] = round(median_ms, 2) + print(f" {'worker_cas_claim':35s} {median_ms:10.2f} ms", flush=True) + return results + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--rows", type=int, default=1_000_000) + parser.add_argument("--seed-only", action="store_true") + parser.add_argument("--json", dest="json_path", default=None, help="also write results to this JSON file") + args = parser.parse_args() + + from archivebox.config.django import setup_django + from archivebox.misc.db import database_exists + + setup_django() + assert database_exists(), "run archivebox init in this directory first" + from django.db import connection + + print(f"backend: {connection.vendor}, target rows: {args.rows}", flush=True) + print("seeding...", flush=True) + seed(args.rows) + if args.seed_only: + return + print("running ANALYZE...", flush=True) + analyze_tables() + print("benchmarks (median of 5):", flush=True) + results = run_benchmarks(args.rows) + results["_backend"] = connection.vendor + results["_rows"] = args.rows + if args.json_path: + with open(args.json_path, "w", encoding="utf-8") as f: + json.dump(results, f, indent=2) + + +if __name__ == "__main__": + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + main() diff --git a/docs/Configuration.md b/docs/Configuration.md index 5ed4dca6..5a996bba 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -666,9 +666,9 @@ Skip the startup check that verifies [`TMP_DIR`](#tmp_dir) can host unix-domain ## Database Settings -*Options for tuning the SQLite index database that backs ArchiveBox's snapshot, tag, and crawl metadata.* +*Options for choosing and tuning the index database that backs ArchiveBox's snapshot, tag, and crawl metadata.* -ArchiveBox stores all of its index metadata in a single SQLite database file (`index.sqlite3` inside your data directory). The defaults are tuned for nearly all users — the knobs below mostly govern **lock-contention behavior**, which matters when multiple workers touch the database concurrently (e.g. supervised orchestrators, parallel `archivebox add` runs, container restarts that race against an in-flight write, or long-running web/admin processes alongside CLI commands). +ArchiveBox stores all of its index metadata in a single SQLite database file (`index.sqlite3` inside your data directory) by default, or optionally in a PostgreSQL database (see [`DATABASE_ENGINE`](#database_engine)). The defaults are tuned for nearly all users — the knobs below mostly govern **lock-contention behavior**, which matters when multiple workers touch the database concurrently (e.g. supervised orchestrators, parallel `archivebox add` runs, container restarts that race against an in-flight write, or long-running web/admin processes alongside CLI commands). > [!NOTE] > These are advanced operator tuning options. If you are not actively diagnosing `database is locked` errors or planning a non-default storage layout, you can safely leave everything in this section at its default. @@ -678,6 +678,49 @@ ArchiveBox stores all of its index metadata in a single SQLite database file (`i - https://www.sqlite.org/wal.html - https://www.sqlite.org/pragma.html +--- + + +#### `DATABASE_ENGINE` +**Possible Values:** [`sqlite`]/`postgres` +Which database backend to use for the main index. Settable as `ARCHIVEBOX_DATABASE_ENGINE` or under `[DATABASE_CONFIG]` in `ArchiveBox.conf`. + +The default `sqlite` keeps everything in a single `index.sqlite3` file inside the data directory and requires no external services. Set this to `postgres` to store the index in a PostgreSQL database instead — useful for large collections with many concurrent writers, or when the data directory lives on a filesystem where SQLite performs poorly (e.g. network mounts). + +With `postgres`, connection details come from the options below. `archivebox init` will create the configured database automatically if the server is reachable and the database does not exist yet. Only the *index* moves to PostgreSQL — snapshot output files stay in `./archive/` in the data directory, and plugin-owned sidecar databases (e.g. the `search.sqlite3` full-text index) are unaffected. + +```ini +# example ArchiveBox.conf +[DATABASE_CONFIG] +DATABASE_ENGINE = postgres +DATABASE_NAME = archivebox +DATABASE_HOST = 127.0.0.1 +DATABASE_PORT = 5432 +DATABASE_USER = archivebox +DATABASE_PASSWORD = s3cret +``` + +> [!WARNING] +> Pick a backend when you first run `archivebox init` and stick with it. There is no built-in tool (yet) to move an existing collection's index between SQLite and PostgreSQL. + +*Related options:* +[`DATABASE_NAME`](#database_name), [`DATABASE_HOST`](#database_host) + +--- + + + + + + + + +#### `DATABASE_HOST` / `DATABASE_PORT` / `DATABASE_USER` / `DATABASE_PASSWORD` +**Possible Values:** [`127.0.0.1`] / [`5432`] / [`archivebox`] / [empty] +PostgreSQL connection settings, used only when [`DATABASE_ENGINE`](#database_engine)`=postgres`. Settable as `ARCHIVEBOX_DATABASE_HOST`, `ARCHIVEBOX_DATABASE_PORT`, `ARCHIVEBOX_DATABASE_USER`, and `ARCHIVEBOX_DATABASE_PASSWORD`. + +`DATABASE_HOST` may also be a path to a directory containing a PostgreSQL unix socket (e.g. `/var/run/postgresql`). When `DATABASE_ENGINE=postgres`, [`DATABASE_NAME`](#database_name) is the name of the PostgreSQL database (default: `archivebox`) instead of a file path. + --- diff --git a/pyproject.toml b/pyproject.toml index bcca18a7..3dd706da 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ dependencies = [ ### Django libraries "setuptools>=74.1.0", # for: django 5 on python >=3.12, distutils is no longer in stdlib but django 5.1 expects distutils (TODO: check if this can be removed eventually) "django>=6.0", + "psycopg[binary]>=3.2", # for: PostgreSQL database backend (ARCHIVEBOX_DATABASE_ENGINE=postgres) "daphne>=4.2.1", # ASGI server for Django (no channels needed - websockets not used) "django-ninja>=1.5.1", "django-extensions>=3.2.3", diff --git a/uv.lock b/uv.lock index 59a779b9..cf8849a3 100644 --- a/uv.lock +++ b/uv.lock @@ -13,11 +13,11 @@ supported-markers = [ ] [options] -exclude-newer = "2026-07-18T13:51:28.491653Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P5D" [options.exclude-newer-package] -abxbus = { timestamp = "2026-07-23T13:51:27.491669Z", span = "PT1S" } +abxbus = { timestamp = "0001-01-01T00:00:00Z", span = "PT1S" } abx-plugins = "2100-01-01T00:00:00Z" abx-dl = "2100-01-01T00:00:00Z" abxpkg = "2100-01-01T00:00:00Z" @@ -27,17 +27,17 @@ name = "abx-dl" version = "1.11.268" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "abx-plugins", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "abxbus", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "abxpkg", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "jambo", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pydantic-settings", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "abx-plugins" }, + { name = "abxbus" }, + { name = "abxpkg" }, + { name = "jambo" }, + { name = "platformdirs" }, + { name = "psutil" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "requests" }, + { name = "rich" }, + { name = "rich-click" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ea/b3/ba83c367c76c1fa82e8e94f468f093d092f52f13a31c468a56436f388442/abx_dl-1.11.268.tar.gz", hash = "sha256:e8cb9ce008f2d0dfdc36adbd54c692caf505b78f5ab8510fe77eda7c4a44699b", size = 86729, upload-time = "2026-07-23T12:12:52.806Z" } wheels = [ @@ -49,13 +49,13 @@ name = "abx-plugins" version = "1.11.311" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "abxbus", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "abxpkg", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "imagesize", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "jambo", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "uv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "abxbus" }, + { name = "abxpkg" }, + { name = "httpx" }, + { name = "imagesize" }, + { name = "jambo" }, + { name = "rich-click" }, + { name = "uv" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/f8/ad69bed9751ccbdc892ac3fd6a63b9eace0bd7b0bbb254b855fd8351bc58/abx_plugins-1.11.311.tar.gz", hash = "sha256:acf3ad909f63f610b8981cf9796ff758d713585a380dd9404f42c8419f1ef020", size = 266481, upload-time = "2026-07-23T11:32:48.583Z" } wheels = [ @@ -67,9 +67,9 @@ name = "abxbus" version = "2.5.40" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "uuid7", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic" }, + { name = "typing-extensions" }, + { name = "uuid7" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/c4/bf7415d0e158cc76860264b3e66a409872541544059f0833e057ae4dcfaf/abxbus-2.5.40.tar.gz", hash = "sha256:f9660128149f9079d18cba1ddbd0daa1941246ac0d19bbfff05491ba417e6447", size = 133584, upload-time = "2026-07-22T03:15:20.732Z" } wheels = [ @@ -81,11 +81,11 @@ name = "abxpkg" version = "1.11.288" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pip", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pip" }, + { name = "platformdirs" }, + { name = "pydantic" }, + { name = "rich-click" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8d/e0/f98aab6246c6d328ee289ced80e95befd46d8046abbbfc317ccf696582b1/abxpkg-1.11.288.tar.gz", hash = "sha256:d802c34ee655cc17930572b07a4ed9f62c18022fe98f380b38fb043dc7ba26b1", size = 212909, upload-time = "2026-07-23T03:30:24.528Z" } wheels = [ @@ -115,7 +115,7 @@ name = "anyio" version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ @@ -127,98 +127,99 @@ name = "archivebox" version = "0.9.35rc138" source = { editable = "." } dependencies = [ - { name = "abx-dl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "abx-plugins", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "abxbus", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "abxpkg", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "atomicwrites", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "base32-crockford", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "croniter", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "daphne", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "dateparser", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-admin-data-views", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-ninja", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-object-actions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-signal-webhooks", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "ipython", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "py-machineid", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pydantic-settings", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "python-statemachine", extra = ["diagrams"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sonic-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "supervisor", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "toml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "tzdata", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "uuid7", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux')" }, - { name = "w3lib", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "abx-dl" }, + { name = "abx-plugins" }, + { name = "abxbus" }, + { name = "abxpkg" }, + { name = "atomicwrites" }, + { name = "base32-crockford" }, + { name = "bleach" }, + { name = "click" }, + { name = "croniter" }, + { name = "daphne" }, + { name = "dateparser" }, + { name = "django" }, + { name = "django-admin-data-views" }, + { name = "django-extensions" }, + { name = "django-ninja" }, + { name = "django-object-actions" }, + { name = "django-signal-webhooks" }, + { name = "django-stubs" }, + { name = "ipython" }, + { name = "platformdirs" }, + { name = "psutil" }, + { name = "psycopg", extra = ["binary"] }, + { name = "py-machineid" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "python-statemachine", extra = ["diagrams"] }, + { name = "requests" }, + { name = "rich" }, + { name = "rich-click" }, + { name = "setuptools" }, + { name = "sonic-client" }, + { name = "supervisor" }, + { name = "toml" }, + { name = "tzdata" }, + { name = "uuid7", marker = "python_full_version < '3.14'" }, + { name = "w3lib" }, ] [package.optional-dependencies] all = [ - { name = "django-auth-ldap", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-autotyping", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-debug-toolbar", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "djdt-flamegraph", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "ipdb", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "python-ldap", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "requests-tracker", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-auth-ldap" }, + { name = "django-autotyping" }, + { name = "django-debug-toolbar" }, + { name = "djdt-flamegraph" }, + { name = "ipdb" }, + { name = "python-ldap" }, + { name = "requests-tracker" }, ] debug = [ - { name = "django-autotyping", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-debug-toolbar", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "djdt-flamegraph", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "ipdb", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "requests-tracker", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-autotyping" }, + { name = "django-debug-toolbar" }, + { name = "djdt-flamegraph" }, + { name = "ipdb" }, + { name = "requests-tracker" }, ] ldap = [ - { name = "django-auth-ldap", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "python-ldap", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-auth-ldap" }, + { name = "python-ldap" }, ] [package.dev-dependencies] dev = [ - { name = "bottle", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "bumpver", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "coverage", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-debug-toolbar", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "djdt-flamegraph", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "ipdb", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "linkify-it-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "logfire", extra = ["django"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "myst-parser", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-instrumentation-django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-instrumentation-sqlite3", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "prek", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pyright", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pytest-codeblocks", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pytest-cov", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pytest-django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pytest-github-actions-annotate-failures", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pytest-httpserver", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pytest-sugar", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pytest-timeout", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "recommonmark", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "requests-tracker", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "ruff", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinx-autodoc2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinx-rtd-theme", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinxcontrib-mermaid", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "ty", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "uv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "viztracer", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "bottle" }, + { name = "bumpver" }, + { name = "coverage" }, + { name = "django-debug-toolbar" }, + { name = "djdt-flamegraph" }, + { name = "ipdb" }, + { name = "linkify-it-py" }, + { name = "logfire", extra = ["django"] }, + { name = "myst-parser" }, + { name = "opentelemetry-instrumentation-django" }, + { name = "opentelemetry-instrumentation-sqlite3" }, + { name = "prek" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-codeblocks" }, + { name = "pytest-cov" }, + { name = "pytest-django" }, + { name = "pytest-github-actions-annotate-failures" }, + { name = "pytest-httpserver" }, + { name = "pytest-sugar" }, + { name = "pytest-timeout" }, + { name = "recommonmark" }, + { name = "requests-tracker" }, + { name = "ruff" }, + { name = "sphinx" }, + { name = "sphinx-autodoc2" }, + { name = "sphinx-rtd-theme" }, + { name = "sphinxcontrib-mermaid" }, + { name = "ty" }, + { name = "uv" }, + { name = "viztracer" }, ] [package.metadata] @@ -250,6 +251,7 @@ requires-dist = [ { name = "ipython", specifier = ">=8.27.0" }, { name = "platformdirs", specifier = ">=4.3.6" }, { name = "psutil", specifier = ">=6.0.0" }, + { name = "psycopg", extras = ["binary"], specifier = ">=3.2" }, { name = "py-machineid", specifier = ">=0.6.0" }, { name = "pydantic", specifier = ">=2.8.0" }, { name = "pydantic-settings", specifier = ">=2.5.2" }, @@ -351,14 +353,14 @@ name = "autobahn" version = "26.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cbor2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "hyperlink", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "msgpack", marker = "(platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_python_implementation == 'CPython' and sys_platform == 'linux')" }, - { name = "txaio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "u-msgpack-python", marker = "(platform_python_implementation != 'CPython' and sys_platform == 'darwin') or (platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, - { name = "ujson", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "cbor2" }, + { name = "cffi" }, + { name = "cryptography" }, + { name = "hyperlink" }, + { name = "msgpack", marker = "platform_python_implementation == 'CPython'" }, + { name = "txaio" }, + { name = "u-msgpack-python", marker = "platform_python_implementation != 'CPython'" }, + { name = "ujson" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/73/f109f563c27e048e45d135d81af19e6ca391e24905550b06bd1c9d674c57/autobahn-26.7.1.tar.gz", hash = "sha256:c6949a2c6eb95fb1c218837dbda0a59abbbebafb8b11098551c01a7061dfd245", size = 14056542, upload-time = "2026-07-15T19:14:01.246Z" } wheels = [ @@ -406,7 +408,7 @@ name = "bleach" version = "6.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "webencodings", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "webencodings" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } wheels = [ @@ -427,10 +429,10 @@ name = "bumpver" version = "2026.1132" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "lexid", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "toml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "click" }, + { name = "colorama" }, + { name = "lexid" }, + { name = "toml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/22/c0c9bf9e7ad6877e4c7c3dedd464e390d9fadb8927c61d6d77547eb17539/bumpver-2026.1132.tar.gz", hash = "sha256:80b223c23fca9bc9dd569b7a44680949d34bee23a738860d9a9b36f1abe3b0e0", size = 116784, upload-time = "2026-05-22T18:40:32.333Z" } wheels = [ @@ -474,7 +476,7 @@ name = "cffi" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "(implementation_name != 'PyPy' and sys_platform == 'darwin') or (implementation_name != 'PyPy' and sys_platform == 'linux')" }, + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } wheels = [ @@ -652,7 +654,7 @@ name = "croniter" version = "6.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "python-dateutil" }, ] sdist = { url = "https://files.pythonhosted.org/packages/37/57/2e2a65aee2a70483cb28e2b7e15a072d00a523207593b44400d4717bb100/croniter-6.2.4.tar.gz", hash = "sha256:fc124f751b1b04805c2a04b061898b436b45ab2320b045e1e052ea895de65189", size = 166267, upload-time = "2026-07-10T09:52:59.955Z" } wheels = [ @@ -664,7 +666,7 @@ name = "cryptography" version = "49.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "(platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_python_implementation != 'PyPy' and sys_platform == 'linux')" }, + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } wheels = [ @@ -711,9 +713,9 @@ name = "daphne" version = "4.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "autobahn", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "twisted", extra = ["tls"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "asgiref" }, + { name = "autobahn" }, + { name = "twisted", extra = ["tls"] }, ] sdist = { url = "https://files.pythonhosted.org/packages/64/d3/65ff32c01cc64d44441b038dbb7cfb0c6a5507a1c937b3d41bd99af7bdc4/daphne-4.2.2.tar.gz", hash = "sha256:6c3527d4ce32630ae054dfb0ef5578e9a35d2f39f0ebcd02ef4f9129a121ce8d", size = 47601, upload-time = "2026-06-03T10:53:13.31Z" } wheels = [ @@ -725,10 +727,10 @@ name = "dateparser" version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pytz", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "tzlocal", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "regex" }, + { name = "tzlocal" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d3/f4/561c49bca97af561d34eed27e3e831135eb5cb88e754c1150be41820f5c6/dateparser-1.4.1.tar.gz", hash = "sha256:f265df13c0380e2e07543ba74b67c0681aaa1096981ffcd35227e1aa0cb81c7c", size = 314734, upload-time = "2026-06-15T08:45:47.659Z" } wheels = [ @@ -749,8 +751,8 @@ name = "django" version = "6.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "asgiref" }, + { name = "sqlparse" }, ] sdist = { url = "https://files.pythonhosted.org/packages/89/55/664f24ff81c9ea19cb7dfc851afeae1f3c2390c7aee01d4ded68b5c1580d/django-6.0.7.tar.gz", hash = "sha256:2998503fc083124fb58037084bfa00de323c7c743f05f1b4284e77bff0ab8890", size = 10921299, upload-time = "2026-07-07T13:51:26.485Z" } wheels = [ @@ -762,8 +764,8 @@ name = "django-admin-data-views" version = "0.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-settings-holder", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django" }, + { name = "django-settings-holder" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/3f/2377a4088c0bf7ce677bb0a526cbf898a11e5528941d7cda303efef3bd73/django_admin_data_views-0.4.3.tar.gz", hash = "sha256:bd287a5d874febd8b544f83b47d0846fbf7b3e00a7f6633912630053c7ae4298", size = 12519, upload-time = "2024-11-24T14:18:00.406Z" } wheels = [ @@ -775,8 +777,8 @@ name = "django-auth-ldap" version = "5.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "python-ldap", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django" }, + { name = "python-ldap" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/6d/d3ceb4b49e7153811a4b2d92bbe198a5ef2e2820469add3d6dc129ef2fab/django_auth_ldap-5.3.0.tar.gz", hash = "sha256:743d8107b146240b46f7e97207dc06cb11facc0cd70dce490b7ca09dd5643d19", size = 55272, upload-time = "2025-12-26T15:00:14.272Z" } wheels = [ @@ -788,8 +790,8 @@ name = "django-autotyping" version = "0.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "libcst", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django" }, + { name = "libcst" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7b/d4/65d2b1c54f35116bd2d31d1064c523ded729353633389ecfc283a93b4c47/django_autotyping-0.5.1.tar.gz", hash = "sha256:b48c57d3d358a608109dd47698e64466e596983e8729bff130669dd744588c25", size = 78974, upload-time = "2024-05-29T14:48:28.561Z" } wheels = [ @@ -801,8 +803,8 @@ name = "django-debug-toolbar" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django" }, + { name = "sqlparse" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1e/ac/3ac674f99c64bfbcd9ae3d7f5f3577f23ea52884e5ac36842e1f024dce03/django_debug_toolbar-7.0.0.tar.gz", hash = "sha256:ef7494c5b459c149e87cc2da88d86e944064945e86bd7b2d879093586b5fefbf", size = 359560, upload-time = "2026-06-19T00:21:23.346Z" } wheels = [ @@ -814,7 +816,7 @@ name = "django-extensions" version = "4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/b3/ed0f54ed706ec0b54fd251cc0364a249c6cd6c6ec97f04dc34be5e929eac/django_extensions-4.1.tar.gz", hash = "sha256:7b70a4d28e9b840f44694e3f7feb54f55d495f8b3fa6c5c0e5e12bcb2aa3cdeb", size = 283078, upload-time = "2025-04-11T01:15:39.617Z" } wheels = [ @@ -826,8 +828,8 @@ name = "django-ninja" version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django" }, + { name = "pydantic" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/7c/3307e17b872f545c88314b2737a22f965785dfb5a120d739b0131d0492c3/django_ninja-1.6.2.tar.gz", hash = "sha256:d56ae5aa4791068ef4ac9a66cfdf2fc11f507413ded35abb79c51d0d52ad6412", size = 3685599, upload-time = "2026-03-18T20:06:47.284Z" } wheels = [ @@ -848,7 +850,7 @@ name = "django-settings-holder" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/810c15a408987262bf70fc1892d4d5c3b2c1ae62e8715b73592c8f46ed37/django_settings_holder-0.3.0.tar.gz", hash = "sha256:d41eb6d6023d61c08e395f2406fd6f047d1edff2f0346d04323f3681f12372ef", size = 8580, upload-time = "2025-04-27T13:15:42.069Z" } wheels = [ @@ -860,11 +862,11 @@ name = "django-signal-webhooks" version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-settings-holder", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "asgiref" }, + { name = "cryptography" }, + { name = "django" }, + { name = "django-settings-holder" }, + { name = "httpx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/15/865e72e1da78bc6c6865ff16b0dffb11db62999fc91bed8c3c1668eac4c1/django_signal_webhooks-0.3.1.tar.gz", hash = "sha256:23dc439be2fdea24b746726495eb1a7a59440809056482eebceb153d050a3f5b", size = 17806, upload-time = "2024-10-31T23:34:40.37Z" } wheels = [ @@ -876,10 +878,10 @@ name = "django-stubs" version = "6.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "django-stubs-ext", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "types-pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django" }, + { name = "django-stubs-ext" }, + { name = "types-pyyaml" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/90/087c6e424e705e05182e543ef6b366a59eb5c92ab008b0dbbba55f357a40/django_stubs-6.0.7.tar.gz", hash = "sha256:bc55431c0af745a64e39cf33a8d36c87dccbedeae2fe26fab47dd355270e8538", size = 282293, upload-time = "2026-07-14T10:08:27.122Z" } wheels = [ @@ -891,8 +893,8 @@ name = "django-stubs-ext" version = "6.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/50/917f7224ea470e89cdcdc93d3dfe75b8391adf976cf12f2ecdb5f5d122be/django_stubs_ext-6.0.7.tar.gz", hash = "sha256:c3172c5126614fd2a44d0196b313b44c21f717cb09477ba52b447d41f4ce613e", size = 6665, upload-time = "2026-07-14T10:07:56.933Z" } wheels = [ @@ -931,8 +933,8 @@ name = "email-validator" version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "dnspython", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "dnspython" }, + { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } wheels = [ @@ -953,7 +955,7 @@ name = "googleapis-common-protos" version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ @@ -974,8 +976,8 @@ name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "h11", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "certifi" }, + { name = "h11" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ @@ -987,10 +989,10 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "httpcore", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ @@ -1002,7 +1004,7 @@ name = "hyperlink" version = "21.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "idna" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } wheels = [ @@ -1032,7 +1034,7 @@ name = "incremental" version = "24.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "packaging" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/3c/82e84109e02c492f382c711c58a3dd91badda6d746def81a1465f74dc9f5/incremental-24.11.0.tar.gz", hash = "sha256:87d3480dbb083c1d736222511a8cf380012a8176c2456d01ef483242abbbcf8c", size = 24000, upload-time = "2025-11-28T02:30:17.861Z" } wheels = [ @@ -1053,8 +1055,8 @@ name = "ipdb" version = "0.13.13" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "decorator", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "ipython", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "decorator" }, + { name = "ipython" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } wheels = [ @@ -1066,16 +1068,16 @@ name = "ipython" version = "9.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "decorator", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "ipython-pygments-lexers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "jedi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "matplotlib-inline", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pexpect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "prompt-toolkit", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "stack-data", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect" }, + { name = "prompt-toolkit" }, + { name = "psutil" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" } wheels = [ @@ -1087,7 +1089,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -1099,9 +1101,9 @@ name = "jambo" version = "0.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "email-validator", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "jsonschema", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "email-validator" }, + { name = "jsonschema" }, + { name = "pydantic" }, ] sdist = { url = "https://files.pythonhosted.org/packages/91/f5/74de157c7aece6a070f99f18201a0e2f46cdfd0f9e337efd411745ed9b22/jambo-0.1.7.tar.gz", hash = "sha256:df89ab8209ebdf7a6e92252ec925979cd3d32811bf4a8182a97dc35b7df58f74", size = 137822, upload-time = "2026-01-14T19:17:30.302Z" } @@ -1110,7 +1112,7 @@ name = "jedi" version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "parso", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "parso" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } wheels = [ @@ -1122,7 +1124,7 @@ name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ @@ -1134,10 +1136,10 @@ name = "jsonschema" version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "jsonschema-specifications", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "referencing", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "rpds-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ @@ -1149,7 +1151,7 @@ name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "referencing", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "referencing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ @@ -1170,8 +1172,8 @@ name = "libcst" version = "1.8.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyyaml", marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform == 'linux')" }, - { name = "pyyaml-ft", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux')" }, + { name = "pyyaml", marker = "python_full_version >= '3.14'" }, + { name = "pyyaml-ft", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/cd/337df968b38d94c5aabd3e1b10630f047a2b345f6e1d4456bd9fe7417537/libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b", size = 891354, upload-time = "2025-11-03T22:33:30.621Z" } wheels = [ @@ -1206,7 +1208,7 @@ name = "linkify-it-py" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "uc-micro-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "uc-micro-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } wheels = [ @@ -1218,13 +1220,13 @@ name = "logfire" version = "4.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "executing", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-exporter-otlp-proto-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "executing" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-sdk" }, + { name = "protobuf" }, + { name = "rich" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/f2/34b8ebbd6bbd82c71055d6b881b24d8ada79a0e6692d3dd8cca5e86fadb3/logfire-4.37.0.tar.gz", hash = "sha256:7ee0cb64b59c356a41a1701fb84597037f8db1fa15df7a3715ef363e5a1de06a", size = 1212176, upload-time = "2026-06-12T20:47:06.904Z" } wheels = [ @@ -1233,8 +1235,8 @@ wheels = [ [package.optional-dependencies] django = [ - { name = "opentelemetry-instrumentation-asgi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-instrumentation-django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-instrumentation-asgi" }, + { name = "opentelemetry-instrumentation-django" }, ] [[package]] @@ -1242,7 +1244,7 @@ name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mdurl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ @@ -1294,7 +1296,7 @@ name = "matplotlib-inline" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "traitlets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } wheels = [ @@ -1306,7 +1308,7 @@ name = "mdit-py-plugins" version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "markdown-it-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } wheels = [ @@ -1359,12 +1361,12 @@ name = "myst-parser" version = "5.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "markdown-it-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "mdit-py-plugins", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "docutils" }, + { name = "jinja2" }, + { name = "markdown-it-py" }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } wheels = [ @@ -1394,7 +1396,7 @@ name = "opentelemetry-api" version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } wheels = [ @@ -1406,7 +1408,7 @@ name = "opentelemetry-exporter-otlp-proto-common" version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-proto", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-proto" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } wheels = [ @@ -1418,13 +1420,13 @@ name = "opentelemetry-exporter-otlp-proto-http" version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "googleapis-common-protos", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-exporter-otlp-proto-common", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-proto", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-common" }, + { name = "opentelemetry-proto" }, + { name = "opentelemetry-sdk" }, + { name = "requests" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } wheels = [ @@ -1436,10 +1438,10 @@ name = "opentelemetry-instrumentation" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "packaging" }, + { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" } wheels = [ @@ -1451,11 +1453,11 @@ name = "opentelemetry-instrumentation-asgi" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-util-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "asgiref" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a0/b5/7ea3a9fd1b80e89786c14250bfaecf32a753c3fd08232690f4da8dc16e29/opentelemetry_instrumentation_asgi-0.63b1.tar.gz", hash = "sha256:267b422416d768f3c7f4054883b41d9c3a7c943d86d20032b738c99a3dbb5862", size = 26151, upload-time = "2026-05-21T16:36:18.368Z" } wheels = [ @@ -1467,10 +1469,10 @@ name = "opentelemetry-instrumentation-dbapi" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "wrapt" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/bf/2bb8048a3ba5686bb70e5d3d7dd2aa2b3d838ccd324e88557780f86b7635/opentelemetry_instrumentation_dbapi-0.63b1.tar.gz", hash = "sha256:406978ed56bcfc5fd246fd918e6b36d0f5de26fa396c78cf63326a7b530597c8", size = 19323, upload-time = "2026-05-21T16:36:27.036Z" } wheels = [ @@ -1482,11 +1484,11 @@ name = "opentelemetry-instrumentation-django" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-instrumentation-wsgi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-util-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-wsgi" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f7/37/90fd5f0dc4a2042dfdb609a9dbe42fde26b3690f9c33ece529b024251015/opentelemetry_instrumentation_django-0.63b1.tar.gz", hash = "sha256:f2071d2f92e4779c5a14dd452b0dfe426343599e6efa9d888304fb639a9f3101", size = 25565, upload-time = "2026-05-21T16:36:27.727Z" } wheels = [ @@ -1498,9 +1500,9 @@ name = "opentelemetry-instrumentation-sqlite3" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-instrumentation-dbapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-instrumentation-dbapi" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/9c/b741eb1d9c6c74680913d0a4fd233c518e8f3f69a200a176121db795ccca/opentelemetry_instrumentation_sqlite3-0.63b1.tar.gz", hash = "sha256:3d61afda8358dc32135fabd27e5934bd25ea0eed68d64c34508f24ba8d723efc", size = 8420, upload-time = "2026-05-21T16:36:47.889Z" } wheels = [ @@ -1512,10 +1514,10 @@ name = "opentelemetry-instrumentation-wsgi" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-util-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-util-http" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/55/832f287fb153adc25c05bc2594d00ac4d1dbeca8b19388b2666d5154c912/opentelemetry_instrumentation_wsgi-0.63b1.tar.gz", hash = "sha256:03d61c4678ce82402e7f37b6a3dbd84cb97b85b3cb416a78c2e74c7c6d9451fa", size = 19667, upload-time = "2026-05-21T16:36:53.873Z" } wheels = [ @@ -1527,7 +1529,7 @@ name = "opentelemetry-proto" version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "protobuf" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } wheels = [ @@ -1539,9 +1541,9 @@ name = "opentelemetry-sdk" version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } wheels = [ @@ -1553,8 +1555,8 @@ name = "opentelemetry-semantic-conventions" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } wheels = [ @@ -1593,7 +1595,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "ptyprocess" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -1653,7 +1655,7 @@ name = "prompt-toolkit" version = "3.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "wcwidth", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "wcwidth" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ @@ -1695,6 +1697,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, ] +[[package]] +name = "psycopg" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/2f/cb91e5502ec9de1de6f1b76cfbf69531932725361168bb06963620c77e2e/psycopg-3.3.4.tar.gz", hash = "sha256:e21207764952cff81b6b8bdacad9a3939f2793367fdac2987b3aac36a651b5bc", size = 165799, upload-time = "2026-05-01T23:31:55.179Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/e0/7b3dee031daae7743609ce3c746565d4a3ed7c2c186479eb48e34e838c64/psycopg-3.3.4-py3-none-any.whl", hash = "sha256:b6bbc25ccf05c8fad3b061d9db2ef0909a555171b84b07f29458a447253d679a", size = 213001, upload-time = "2026-05-01T23:20:50.816Z" }, +] + +[package.optional-dependencies] +binary = [ + { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, +] + +[[package]] +name = "psycopg-binary" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/43/13e9c406fbbf354580476e248a16b64802a376873ebe6339e30bb655572d/psycopg_binary-3.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbd1d4ed566895ad2d3bf4ddfd8bae90026930ddf29df3b9d91d32c8c47866a7", size = 4590377, upload-time = "2026-05-01T23:29:18.782Z" }, + { url = "https://files.pythonhosted.org/packages/22/be/2923cd7c3683e7afdecf4f10796a18de02f5c5ddc0969aa2ad0a8cdd3bbd/psycopg_binary-3.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:75a9067e236f9b9ae3535b66fe99bddb33d39c0de10112e49b9ab11eee53dc31", size = 4669023, upload-time = "2026-05-01T23:29:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/96/a0/2c913d6fe13d6a8bd13597d36739bf47af063ad9399e402cfecab16f3c1e/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b56b603ebcea8aa10b46228b8410ba7f13e7c2ee54389d4d9be0927fd8ce2a70", size = 5467423, upload-time = "2026-05-01T23:29:33.416Z" }, + { url = "https://files.pythonhosted.org/packages/e7/38/205d10bc1ad0df4a21c5c51659126bd3ea0ef98fcad1e852f78c249bb9c3/psycopg_binary-3.3.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c677c4ad433cb7150c8cd304a0769ae3bcfbe5ea0676eb53faa7b1443b16d0d3", size = 5151137, upload-time = "2026-05-01T23:29:42.013Z" }, + { url = "https://files.pythonhosted.org/packages/36/fc/f0381ddcd45eff3bb70dbca6823a996048d7f507b2ec3fc92c6fabc0fe87/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26df2717e59c0473e4465a97dfb1b7afebaa479277870fd5784d1436470db47c", size = 6736671, upload-time = "2026-05-01T23:29:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/95/40/fa545ae152c24327651e5624e4902121e808270be36c10b12e9939be09bc/psycopg_binary-3.3.4-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dc1f79fd16bb1f3f4421417a514607539f17804d95c7ed617265369d1981cae", size = 4979601, upload-time = "2026-05-01T23:29:56.961Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/2f8a47ee97f90cd2b933d0463081d35631ff419de2b8c984a5f369857de0/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:136f199a407b5348b9b857c504aff60c77622a28482e7195839ce1b51238c4cc", size = 4510513, upload-time = "2026-05-01T23:30:07.243Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/94e842ff4a7f98ed162580ca2e8b8864b28c1e0350f2443f8ee47f821167/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b6f5a29e9c775b9f12a1a717aa7a2c80f9e1db6f27ba44a5b59c80ac61d2ffcf", size = 4187243, upload-time = "2026-05-01T23:30:15.352Z" }, + { url = "https://files.pythonhosted.org/packages/d0/83/fc6c174b672e29b7de996ea77b6cbddf46c891751c3355f6974292baa6b4/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ee17a2cf4943cde261adfad1bbc5bf38d6b3776d7afff74c7cabcbeaeb08c260", size = 3927347, upload-time = "2026-05-01T23:30:21.186Z" }, + { url = "https://files.pythonhosted.org/packages/e9/65/768364d4a97a15b1a7f47ba52688c1686f22941d8332a8398cefc468e25f/psycopg_binary-3.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5c4ab71be17bdca30cb34c34c4e1496e2f5d6f20c199c12bad226070b22ef9bf", size = 4236393, upload-time = "2026-05-01T23:30:26.211Z" }, + { url = "https://files.pythonhosted.org/packages/48/a6/828c9185701dab71b234c2a76c38a08b098ebfec5020716b4e93807492b5/psycopg_binary-3.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:28b7398fdd19db3232c884fb24550bdfe951221f510e195e233299e4c9b78f97", size = 4607292, upload-time = "2026-05-01T23:30:38.962Z" }, + { url = "https://files.pythonhosted.org/packages/92/58/5b40dbc9d839045c9dae956960e4fb6d20bcabe6c59a2aa34fc3a371913f/psycopg_binary-3.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1fbaa292a3c8bb61b45df1ad3da1908ccee7cb889db9425e3557d9e34e2a4829", size = 4687023, upload-time = "2026-05-01T23:30:47.227Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/793f0ac107a9003b48441d0d1f9f616d96e0f37458dd8dc12528ceff55fb/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94596f9e7633ee3f6440711d43bb70aa31cc0a46a900ab8b4201a366ace5c9e7", size = 5486985, upload-time = "2026-05-01T23:30:55.517Z" }, + { url = "https://files.pythonhosted.org/packages/8f/26/42e8533497e2592334f68ec529cf5f840f7fa4e99575a4bb61aa184dbfbf/psycopg_binary-3.3.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c0056529e68dbe9184cd4019a1f3d8f3a4ead2f6fc7a5afcf27d3314edd1277", size = 5168745, upload-time = "2026-05-01T23:31:01.904Z" }, + { url = "https://files.pythonhosted.org/packages/15/af/b7151776cc08d5935d45c833ec818a9beb417cf7c08239af1aafbdae78ee/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c09aad7051326e7603c14e50636db9c01f78272dc54b3accff03d46370461e6", size = 6761486, upload-time = "2026-05-01T23:31:14.511Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ed/c92533b9124712d592cbf1cd6c76da933a2e0acea81dfe1fbe7e735f0cff/psycopg_binary-3.3.4-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:514404ed543efd620c85602b747df2a23cf1241b4067199e1a66f2d2757aaa41", size = 4997427, upload-time = "2026-05-01T23:31:20.901Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/ccadfd0de416aa188356daa199453af24087b042e296088706d190ae0295/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:46893c26858be12cc49ca4226ed6a60b4bfccadd946b3bebb783a60b38788228", size = 4533549, upload-time = "2026-05-01T23:31:26.204Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a0/c8f43cee36386f7bc891ab41a9d31ea07cf9826038e732da79f26b1e5f34/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:df1d567fc430f6df15c9fcf67d87685fc49bdb325adc0db5af1adfb2f44eb5c9", size = 4210256, upload-time = "2026-05-01T23:31:33.884Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/c1547871be3790676e8868b38655496422f94f0978dfb66b74bdba2f1676/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6b9016b1714da4dd5ecaaa75b82098aa5a0b87854ce9b092e21c27c4ae23e014", size = 3946204, upload-time = "2026-05-01T23:31:39.626Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/f6670f00fa7ea601584623f6c11602ab92117d83eaff885e0210f6de7418/psycopg_binary-3.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:47c656a8a7ba6eb0cff1801a4caaa9c8bdc12d03080e273aff1c8ac39971a77e", size = 4255811, upload-time = "2026-05-01T23:31:44.986Z" }, +] + [[package]] name = "ptyprocess" version = "0.7.0" @@ -1736,7 +1779,7 @@ name = "pyasn1-modules" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyasn1", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyasn1" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } wheels = [ @@ -1757,10 +1800,10 @@ name = "pydantic" version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pydantic-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ @@ -1772,7 +1815,7 @@ name = "pydantic-core" version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ @@ -1819,9 +1862,9 @@ name = "pydantic-settings" version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ @@ -1833,7 +1876,7 @@ name = "pydot" version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyparsing", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyparsing" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/35/b17cb89ff865484c6a20ef46bf9d95a5f07328292578de0b295f4a6beec2/pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5", size = 162594, upload-time = "2025-06-17T20:09:56.454Z" } wheels = [ @@ -1854,7 +1897,7 @@ name = "pyopenssl" version = "26.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "cryptography" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/b7/da07bae88f5a9506b4def6f2f4903cf4c3b8831e560dba8fa18ca08f758f/pyopenssl-26.3.0.tar.gz", hash = "sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341", size = 182024, upload-time = "2026-06-12T20:28:07.458Z" } wheels = [ @@ -1875,8 +1918,8 @@ name = "pyright" version = "1.1.411" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nodeenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "nodeenv" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } wheels = [ @@ -1888,10 +1931,10 @@ name = "pytest" version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "iniconfig", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pluggy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ @@ -1903,7 +1946,7 @@ name = "pytest-codeblocks" version = "0.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/3a/cb/5f40df7db75c0ac2555009287bb97b322f8d79f9f2bfdfb0bdb560834c28/pytest_codeblocks-0.18.0-py3-none-any.whl", hash = "sha256:3fe944dc505107421204c83e9232e0155eea1279b9425a10bee327079b272efb", size = 8009, upload-time = "2026-06-15T20:48:02.608Z" }, @@ -1914,9 +1957,9 @@ name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pluggy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ @@ -1928,7 +1971,7 @@ name = "pytest-django" version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" } wheels = [ @@ -1940,7 +1983,7 @@ name = "pytest-github-actions-annotate-failures" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/a0/bdb91581b03c41016c78e16b8ec36c34d8508206fcb30f1951c9cdff2e97/pytest_github_actions_annotate_failures-0.4.2.tar.gz", hash = "sha256:5dd18304512361788bc7b5c5c805db853f03f4950c6be09b088de6bab8e2e6c9", size = 12158, upload-time = "2026-06-19T15:59:17.445Z" } wheels = [ @@ -1952,7 +1995,7 @@ name = "pytest-httpserver" version = "1.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "werkzeug", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "werkzeug" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/17/ad187f46998814014f7cda309de700b87c0eb4b2e111e18bc8c819be7116/pytest_httpserver-1.1.5.tar.gz", hash = "sha256:dc3d82e1fe00e491829d8939c549bf4bd9b39a260f87113c619b9d517c2f8ff1", size = 70974, upload-time = "2026-02-14T13:27:23.412Z" } wheels = [ @@ -1964,8 +2007,8 @@ name = "pytest-sugar" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "termcolor", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest" }, + { name = "termcolor" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/4e/60fed105549297ba1a700e1ea7b828044842ea27d72c898990510b79b0e2/pytest-sugar-1.1.1.tar.gz", hash = "sha256:73b8b65163ebf10f9f671efab9eed3d56f20d2ca68bda83fa64740a92c08f65d", size = 16533, upload-time = "2025-08-23T12:19:35.737Z" } wheels = [ @@ -1977,7 +2020,7 @@ name = "pytest-timeout" version = "2.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } wheels = [ @@ -1989,7 +2032,7 @@ name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "six" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ @@ -2010,8 +2053,8 @@ name = "python-ldap" version = "3.4.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyasn1", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pyasn1-modules", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyasn1" }, + { name = "pyasn1-modules" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/f4/60edeb794bbc9ed0ff2149bbaeec605f3ed331766459d195832ecbd0ba2d/python_ldap-3.4.7.tar.gz", hash = "sha256:bacd9fb680d20263d8570ade1cf234d90d281149a8beb4f079dd8f33f7613dc8", size = 387477, upload-time = "2026-05-20T13:41:04.358Z" } @@ -2026,7 +2069,7 @@ wheels = [ [package.optional-dependencies] diagrams = [ - { name = "pydot", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydot" }, ] [[package]] @@ -2094,9 +2137,9 @@ name = "recommonmark" version = "0.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "commonmark", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "docutils", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "commonmark" }, + { name = "docutils" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/00/3dd2bdc4184b0ce754b5b446325abf45c2e0a347e022292ddc44670f628c/recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67", size = 34444, upload-time = "2020-12-17T19:24:56.523Z" } wheels = [ @@ -2108,8 +2151,8 @@ name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "rpds-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "attrs" }, + { name = "rpds-py" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ @@ -2181,10 +2224,10 @@ name = "requests" version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "charset-normalizer", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ @@ -2196,8 +2239,8 @@ name = "requests-tracker" version = "0.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django" }, + { name = "sqlparse" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/10/35d214c4eaa479251ebb6f774842e476cd4162ca939e72bb1d943131fb2c/requests_tracker-0.3.3.tar.gz", hash = "sha256:eb288d69ebcae49149b41d603960d101d7eb892627e3455a456fa1f9441d2a49", size = 49168, upload-time = "2023-11-04T01:24:11.992Z" } wheels = [ @@ -2209,8 +2252,8 @@ name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "markdown-it-py" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ @@ -2222,8 +2265,8 @@ name = "rich-click" version = "1.9.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "click" }, + { name = "rich" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f7/ea/21e4867ea0ef881ffd4c0550fc21a061435e50d6324bcd034396633cbc18/rich_click-1.9.8.tar.gz", hash = "sha256:4008f921da88b5d91646c134ec881c1500e5a6b3f093e90e8f29400e09608371", size = 75363, upload-time = "2026-05-28T19:54:59.144Z" } wheels = [ @@ -2334,8 +2377,8 @@ name = "service-identity" version = "26.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "attrs" }, + { name = "cryptography" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/87/ad52e2c582c0f0e7f0a1b86950494c38d67422dc0f5ed9044a5fb9569a49/service_identity-26.1.0.tar.gz", hash = "sha256:6358c52882c96e66ac4a55eb3a72c7dd4a70763f8cc6fa4e70abde2656f4bf3b", size = 42898, upload-time = "2026-05-30T12:04:55.184Z" } wheels = [ @@ -2383,22 +2426,22 @@ name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "alabaster", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "docutils", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "imagesize", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "roman-numerals", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "snowballstemmer", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinxcontrib-applehelp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinxcontrib-devhelp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinxcontrib-htmlhelp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinxcontrib-jsmath", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinxcontrib-qthelp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinxcontrib-serializinghtml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -2410,8 +2453,8 @@ name = "sphinx-autodoc2" version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astroid", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "astroid" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/17/5f/5350046d1aa1a56b063ae08b9ad871025335c9d55fe2372896ea48711da9/sphinx_autodoc2-0.5.0.tar.gz", hash = "sha256:7d76044aa81d6af74447080182b6868c7eb066874edc835e8ddf810735b6565a", size = 115077, upload-time = "2023-11-27T07:27:51.407Z" } wheels = [ @@ -2423,9 +2466,9 @@ name = "sphinx-rtd-theme" version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinxcontrib-jquery", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "docutils" }, + { name = "sphinx" }, + { name = "sphinxcontrib-jquery" }, ] sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } wheels = [ @@ -2464,7 +2507,7 @@ name = "sphinxcontrib-jquery" version = "4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } wheels = [ @@ -2485,9 +2528,9 @@ name = "sphinxcontrib-mermaid" version = "2.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "sphinx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jinja2" }, + { name = "pyyaml" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/51/29/54cf1f7e03630ca4859caba25bc192698954d7c630377c62d1e264715c37/sphinxcontrib_mermaid-2.0.3.tar.gz", hash = "sha256:a6865ef6b65b225c5403a3170de63a04a07227cada11a4a71a6b87b4f9ed185a", size = 20764, upload-time = "2026-07-08T00:30:44.216Z" } wheels = [ @@ -2526,9 +2569,9 @@ name = "stack-data" version = "0.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asttokens", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "executing", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pure-eval", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ @@ -2576,13 +2619,13 @@ name = "twisted" version = "26.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "automat", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "constantly", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "hyperlink", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "incremental", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "zope-interface", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "attrs" }, + { name = "automat" }, + { name = "constantly" }, + { name = "hyperlink" }, + { name = "incremental" }, + { name = "typing-extensions" }, + { name = "zope-interface" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/97/6e9beb1e78247ae6dc34114f27d538cf2cb183c4afcd3609dfdf2b0439c8/twisted-26.4.0.tar.gz", hash = "sha256:dbfd0fe1ee409d0243fdd7a6a6ff14f4948cec1fd78e0376291f805e1501fae9", size = 3575095, upload-time = "2026-05-11T11:24:51.861Z" } wheels = [ @@ -2591,9 +2634,9 @@ wheels = [ [package.optional-dependencies] tls = [ - { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "pyopenssl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "service-identity", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "idna" }, + { name = "pyopenssl" }, + { name = "service-identity" }, ] [[package]] @@ -2650,7 +2693,7 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ @@ -2777,7 +2820,7 @@ name = "viztracer" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "objprint", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "objprint" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9c/ab/94ae463cd4e386f143e3520a274856c4f2b4858d7ae30aa223ae25e9a2e5/viztracer-1.1.1.tar.gz", hash = "sha256:dcd4b5ddcc3a40ee79a584406d984cb4d40bc3301a6c9015d8949d4445fe9346", size = 15667892, upload-time = "2025-11-11T00:03:17.751Z" } wheels = [ @@ -2831,7 +2874,7 @@ name = "werkzeug" version = "3.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "markupsafe" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } wheels = [ From c2191f9e74044a6ddf34f0a8100c15ed7b3d6f74 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 17:18:54 +0000 Subject: [PATCH 2/7] Fix lint: settings import placement and benchmark script exec bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019YHSjZM6TstSAMN2PhgfUg --- archivebox/api/migrations/0001_initial.py | 10 ++++++++-- archivebox/core/settings.py | 8 ++++---- archivebox/crawls/migrations/0001_initial.py | 10 ++++++++-- archivebox/machine/migrations/0001_initial.py | 5 ++++- bin/benchmark_db_backends.py | 13 ++++++++++--- 5 files changed, 34 insertions(+), 12 deletions(-) mode change 100644 => 100755 bin/benchmark_db_backends.py diff --git a/archivebox/api/migrations/0001_initial.py b/archivebox/api/migrations/0001_initial.py index 99cdeef9..b3a2aab3 100644 --- a/archivebox/api/migrations/0001_initial.py +++ b/archivebox/api/migrations/0001_initial.py @@ -72,7 +72,10 @@ def _run_sqlite_only_sql(apps, schema_editor): if schema_editor.connection.vendor != "sqlite": return migrations.RunSQL(sql=INITIAL_SQL, reverse_sql=INITIAL_REVERSE_SQL).database_forwards( - "api", schema_editor, None, None, + "api", + schema_editor, + None, + None, ) @@ -80,7 +83,10 @@ def _run_sqlite_only_sql_reverse(apps, schema_editor): if schema_editor.connection.vendor != "sqlite": return migrations.RunSQL(sql=INITIAL_SQL, reverse_sql=INITIAL_REVERSE_SQL).database_backwards( - "api", schema_editor, None, None, + "api", + schema_editor, + None, + None, ) diff --git a/archivebox/core/settings.py b/archivebox/core/settings.py index b278ee2a..80619bcd 100644 --- a/archivebox/core/settings.py +++ b/archivebox/core/settings.py @@ -14,6 +14,10 @@ import archivebox from archivebox.config.constants import CONSTANTS from archivebox.config.common import get_config from archivebox.core.routes_util import get_api_base_url, get_admin_base_url, get_base_url, normalize_base_url + +# All sqlite-vs-postgres connection logic lives in archivebox.misc.db; +# DATABASE_ENGINE config selects the backend (sqlite by default). +from archivebox.misc.db import get_database_settings, get_sqlite_connection_options from .settings_logging import SETTINGS_LOGGING @@ -212,10 +216,6 @@ TEMPLATES = [ ### External Service Settings ################################################################################ -# All sqlite-vs-postgres connection logic lives in archivebox.misc.db; -# DATABASE_ENGINE config selects the backend (sqlite by default). -from archivebox.misc.db import get_database_settings, get_sqlite_connection_options - DATABASE_NAME = CONFIG.DATABASE_NAME SQLITE_JOURNAL_MODE = CONFIG.SQLITE_JOURNAL_MODE SQLITE_MMAP_SIZE = CONFIG.SQLITE_MMAP_SIZE diff --git a/archivebox/crawls/migrations/0001_initial.py b/archivebox/crawls/migrations/0001_initial.py index 6b611a3b..6a079d30 100644 --- a/archivebox/crawls/migrations/0001_initial.py +++ b/archivebox/crawls/migrations/0001_initial.py @@ -77,7 +77,10 @@ def _run_sqlite_only_sql(apps, schema_editor): if schema_editor.connection.vendor != "sqlite": return migrations.RunSQL(sql=INITIAL_SQL, reverse_sql=INITIAL_REVERSE_SQL).database_forwards( - "crawls", schema_editor, None, None, + "crawls", + schema_editor, + None, + None, ) @@ -85,7 +88,10 @@ def _run_sqlite_only_sql_reverse(apps, schema_editor): if schema_editor.connection.vendor != "sqlite": return migrations.RunSQL(sql=INITIAL_SQL, reverse_sql=INITIAL_REVERSE_SQL).database_backwards( - "crawls", schema_editor, None, None, + "crawls", + schema_editor, + None, + None, ) diff --git a/archivebox/machine/migrations/0001_initial.py b/archivebox/machine/migrations/0001_initial.py index 9425f761..cea929bb 100644 --- a/archivebox/machine/migrations/0001_initial.py +++ b/archivebox/machine/migrations/0001_initial.py @@ -32,7 +32,10 @@ def _pg_sync_schema(apps, schema_editor): from archivebox.misc.db import rebuild_models_from_migration_state rebuild_models_from_migration_state( - apps, schema_editor, "machine", ["Machine", "NetworkInterface", "Binary"] + apps, + schema_editor, + "machine", + ["Machine", "NetworkInterface", "Binary"], ) diff --git a/bin/benchmark_db_backends.py b/bin/benchmark_db_backends.py old mode 100644 new mode 100755 index 5a344f21..09418832 --- a/bin/benchmark_db_backends.py +++ b/bin/benchmark_db_backends.py @@ -129,7 +129,9 @@ def run_benchmarks(rows: int) -> dict[str, float]: benchmarks = { "exact_count": lambda: Snapshot.objects.count(), "approximate_row_counts": lambda: approximate_row_counts(connection), - "admin_list_page": lambda: list(Snapshot.objects.order_by("-bookmarked_at").values("id", "url", "title", "status", "bookmarked_at")[:40]), + "admin_list_page": lambda: list( + Snapshot.objects.order_by("-bookmarked_at").values("id", "url", "title", "status", "bookmarked_at")[:40], + ), "admin_list_page_offset_10k": lambda: list(Snapshot.objects.order_by("-bookmarked_at").values("id", "url", "title")[10_000:10_040]), "snapshot_detail_by_url": lambda: list(Snapshot.objects.filter(fragmentless_q(target_url))[:10]), "snapshot_detail_archiveresults": lambda: list( @@ -137,7 +139,9 @@ def run_benchmarks(rows: int) -> dict[str, float]: ), "url_prefix_search": lambda: list(iter_url_prefix_search_ids("https://site500.example.org/", Snapshot.objects.all())), "worker_queue_scan": lambda: list( - Snapshot.objects.filter(status="queued", retry_at__lte=now).order_by("retry_at", "created_at").values_list("id", flat=True)[:100], + Snapshot.objects.filter(status="queued", retry_at__lte=now) + .order_by("retry_at", "created_at") + .values_list("id", flat=True)[:100], ), "status_facet_counts": lambda: dict(Snapshot.objects.values_list("status").annotate(n=Count("id")).values_list("status", "n")), "tag_join_filter": lambda: list(Snapshot.objects.filter(title__icontains="page 4242").values("id")[:20]), @@ -152,7 +156,10 @@ def run_benchmarks(rows: int) -> dict[str, float]: snapshot = Snapshot.objects.filter(status="queued").order_by("retry_at").first() if snapshot is None: return 0 - return Snapshot.objects.filter(pk=snapshot.pk, retry_at=snapshot.retry_at).update(retry_at=now + timedelta(seconds=60), modified_at=now) + return Snapshot.objects.filter(pk=snapshot.pk, retry_at=snapshot.retry_at).update( + retry_at=now + timedelta(seconds=60), + modified_at=now, + ) median_ms, _ = timed(claim_one) results["worker_cas_claim"] = round(median_ms, 2) From 35145d282cf1f97b0adc2417ce59d6186906cc4c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:23:44 +0000 Subject: [PATCH 3/7] Regenerate lockfile with pinned uv and slim DB adapter helpers Re-lock uv.lock with the CI-pinned uv version so the diff is limited to the psycopg addition (a newer local uv had rewritten platform markers and exclude-newer, breaking `uv sync --locked` in CI). Consolidate the misc/db.py adapter surface: fold database_backend() into is_postgres(), drop the redundant vendor-name constants, remove the unused migration_table_exists() helper, and inline the single-use missing-table check. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019YHSjZM6TstSAMN2PhgfUg --- archivebox/misc/db.py | 36 +-- uv.lock | 690 +++++++++++++++++++++--------------------- 2 files changed, 354 insertions(+), 372 deletions(-) diff --git a/archivebox/misc/db.py b/archivebox/misc/db.py index 746dcd57..f154b8fe 100644 --- a/archivebox/misc/db.py +++ b/archivebox/misc/db.py @@ -30,20 +30,12 @@ from archivebox.misc.util import enforce_types # ``connection.vendor``, building ``DATABASES`` entries, or touching # ``CONSTANTS.DATABASE_FILE`` directly. -SQLITE_VENDOR = "sqlite" -POSTGRES_VENDOR = "postgresql" - - -def database_backend() -> str: - """Configured backend vendor name: 'sqlite' (default) or 'postgresql'.""" - from archivebox.config.common import get_config - - engine = (get_config().DATABASE_ENGINE or "sqlite").strip().lower() - return POSTGRES_VENDOR if engine.startswith("postgres") else SQLITE_VENDOR - def is_postgres() -> bool: - return database_backend() == POSTGRES_VENDOR + """True if DATABASE_ENGINE selects postgres (sqlite is the default).""" + from archivebox.config.common import get_config + + return (get_config().DATABASE_ENGINE or "sqlite").strip().lower().startswith("postgres") def postgres_db_params() -> dict[str, str]: @@ -188,12 +180,6 @@ def ensure_database_ready() -> None: rich_print(f" + Created PostgreSQL database {params['NAME']}") -def is_missing_table_error(err: BaseException) -> bool: - """True if err means a queried table does not exist (any backend).""" - msg = str(err).lower() - return "no such table" in msg or ("relation" in msg and "does not exist" in msg) - - def approximate_row_counts(connection) -> dict[str, int]: """Cheap per-table approximate row counts from the backend's optimizer stats. @@ -204,14 +190,14 @@ def approximate_row_counts(connection) -> dict[str, int]: counts: dict[str, int] = {} try: with connection.cursor() as cursor: - if connection.vendor == SQLITE_VENDOR: + if connection.vendor == "sqlite": cursor.execute("SELECT tbl, stat FROM sqlite_stat1") for table, stat in cursor.fetchall(): try: counts[str(table)] = int(str(stat).split()[0]) except (IndexError, TypeError, ValueError): continue - elif connection.vendor == POSTGRES_VENDOR: + elif connection.vendor == "postgresql": cursor.execute( """ SELECT c.relname, c.reltuples::bigint @@ -249,11 +235,6 @@ def truncate_overlong_charfields(sender, instance, **kwargs) -> None: # --- migration helpers ------------------------------------------------------ -def migration_table_exists(connection, table_name: str) -> bool: - """Portable existence check usable from inside migrations.""" - return table_name in connection.introspection.table_names() - - def migration_table_columns(connection, table_name: str) -> set[str]: """Portable column-name introspection usable from inside migrations.""" with connection.cursor() as cursor: @@ -272,7 +253,7 @@ def rebuild_models_from_migration_state(apps, schema_editor, app_label: str, mod it from state is always equivalent, keeping the real schema in lockstep with migration state at each divergence point. No-op on sqlite. """ - if schema_editor.connection.vendor == SQLITE_VENDOR: + if schema_editor.connection.vendor == "sqlite": return existing_tables = set(schema_editor.connection.introspection.table_names()) models = [apps.get_model(app_label, model_name) for model_name in model_names] @@ -619,7 +600,8 @@ def migration_state(out_dir: Path = CONSTANTS.DATA_DIR) -> tuple[list[str], list try: cursor.execute("SELECT app, name FROM django_migrations") except Exception as err: - if is_missing_table_error(err): + msg = str(err).lower() + if "no such table" in msg or ("relation" in msg and "does not exist" in msg): return set() raise return {(str(app), str(name)) for app, name in cursor.fetchall()} diff --git a/uv.lock b/uv.lock index cf8849a3..81e54c79 100644 --- a/uv.lock +++ b/uv.lock @@ -13,11 +13,11 @@ supported-markers = [ ] [options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. +exclude-newer = "2026-07-19T07:19:43.036580449Z" exclude-newer-span = "P5D" [options.exclude-newer-package] -abxbus = { timestamp = "0001-01-01T00:00:00Z", span = "PT1S" } +abxbus = { timestamp = "2026-07-24T07:19:42.03659601Z", span = "PT1S" } abx-plugins = "2100-01-01T00:00:00Z" abx-dl = "2100-01-01T00:00:00Z" abxpkg = "2100-01-01T00:00:00Z" @@ -27,17 +27,17 @@ name = "abx-dl" version = "1.11.268" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "abx-plugins" }, - { name = "abxbus" }, - { name = "abxpkg" }, - { name = "jambo" }, - { name = "platformdirs" }, - { name = "psutil" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "requests" }, - { name = "rich" }, - { name = "rich-click" }, + { name = "abx-plugins", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "abxbus", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "abxpkg", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jambo", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic-settings", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ea/b3/ba83c367c76c1fa82e8e94f468f093d092f52f13a31c468a56436f388442/abx_dl-1.11.268.tar.gz", hash = "sha256:e8cb9ce008f2d0dfdc36adbd54c692caf505b78f5ab8510fe77eda7c4a44699b", size = 86729, upload-time = "2026-07-23T12:12:52.806Z" } wheels = [ @@ -49,13 +49,13 @@ name = "abx-plugins" version = "1.11.311" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "abxbus" }, - { name = "abxpkg" }, - { name = "httpx" }, - { name = "imagesize" }, - { name = "jambo" }, - { name = "rich-click" }, - { name = "uv" }, + { name = "abxbus", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "abxpkg", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "imagesize", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jambo", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "uv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/f8/ad69bed9751ccbdc892ac3fd6a63b9eace0bd7b0bbb254b855fd8351bc58/abx_plugins-1.11.311.tar.gz", hash = "sha256:acf3ad909f63f610b8981cf9796ff758d713585a380dd9404f42c8419f1ef020", size = 266481, upload-time = "2026-07-23T11:32:48.583Z" } wheels = [ @@ -67,9 +67,9 @@ name = "abxbus" version = "2.5.40" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "typing-extensions" }, - { name = "uuid7" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "uuid7", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/c4/bf7415d0e158cc76860264b3e66a409872541544059f0833e057ae4dcfaf/abxbus-2.5.40.tar.gz", hash = "sha256:f9660128149f9079d18cba1ddbd0daa1941246ac0d19bbfff05491ba417e6447", size = 133584, upload-time = "2026-07-22T03:15:20.732Z" } wheels = [ @@ -81,11 +81,11 @@ name = "abxpkg" version = "1.11.288" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pip" }, - { name = "platformdirs" }, - { name = "pydantic" }, - { name = "rich-click" }, - { name = "typing-extensions" }, + { name = "pip", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8d/e0/f98aab6246c6d328ee289ced80e95befd46d8046abbbfc317ccf696582b1/abxpkg-1.11.288.tar.gz", hash = "sha256:d802c34ee655cc17930572b07a4ed9f62c18022fe98f380b38fb043dc7ba26b1", size = 212909, upload-time = "2026-07-23T03:30:24.528Z" } wheels = [ @@ -115,7 +115,7 @@ name = "anyio" version = "4.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna" }, + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } wheels = [ @@ -127,99 +127,99 @@ name = "archivebox" version = "0.9.35rc138" source = { editable = "." } dependencies = [ - { name = "abx-dl" }, - { name = "abx-plugins" }, - { name = "abxbus" }, - { name = "abxpkg" }, - { name = "atomicwrites" }, - { name = "base32-crockford" }, - { name = "bleach" }, - { name = "click" }, - { name = "croniter" }, - { name = "daphne" }, - { name = "dateparser" }, - { name = "django" }, - { name = "django-admin-data-views" }, - { name = "django-extensions" }, - { name = "django-ninja" }, - { name = "django-object-actions" }, - { name = "django-signal-webhooks" }, - { name = "django-stubs" }, - { name = "ipython" }, - { name = "platformdirs" }, - { name = "psutil" }, - { name = "psycopg", extra = ["binary"] }, - { name = "py-machineid" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "python-statemachine", extra = ["diagrams"] }, - { name = "requests" }, - { name = "rich" }, - { name = "rich-click" }, - { name = "setuptools" }, - { name = "sonic-client" }, - { name = "supervisor" }, - { name = "toml" }, - { name = "tzdata" }, - { name = "uuid7", marker = "python_full_version < '3.14'" }, - { name = "w3lib" }, + { name = "abx-dl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "abx-plugins", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "abxbus", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "abxpkg", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "atomicwrites", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "base32-crockford", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "bleach", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "croniter", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "daphne", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "dateparser", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-admin-data-views", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-ninja", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-object-actions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-signal-webhooks", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-stubs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "ipython", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "psycopg", extra = ["binary"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "py-machineid", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic-settings", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "python-statemachine", extra = ["diagrams"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "setuptools", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sonic-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "supervisor", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "toml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tzdata", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "uuid7", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux')" }, + { name = "w3lib", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] [package.optional-dependencies] all = [ - { name = "django-auth-ldap" }, - { name = "django-autotyping" }, - { name = "django-debug-toolbar" }, - { name = "djdt-flamegraph" }, - { name = "ipdb" }, - { name = "python-ldap" }, - { name = "requests-tracker" }, + { name = "django-auth-ldap", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-autotyping", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-debug-toolbar", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "djdt-flamegraph", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "ipdb", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "python-ldap", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests-tracker", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] debug = [ - { name = "django-autotyping" }, - { name = "django-debug-toolbar" }, - { name = "djdt-flamegraph" }, - { name = "ipdb" }, - { name = "requests-tracker" }, + { name = "django-autotyping", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-debug-toolbar", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "djdt-flamegraph", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "ipdb", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests-tracker", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] ldap = [ - { name = "django-auth-ldap" }, - { name = "python-ldap" }, + { name = "django-auth-ldap", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "python-ldap", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] [package.dev-dependencies] dev = [ - { name = "bottle" }, - { name = "bumpver" }, - { name = "coverage" }, - { name = "django-debug-toolbar" }, - { name = "djdt-flamegraph" }, - { name = "ipdb" }, - { name = "linkify-it-py" }, - { name = "logfire", extra = ["django"] }, - { name = "myst-parser" }, - { name = "opentelemetry-instrumentation-django" }, - { name = "opentelemetry-instrumentation-sqlite3" }, - { name = "prek" }, - { name = "pyright" }, - { name = "pytest" }, - { name = "pytest-codeblocks" }, - { name = "pytest-cov" }, - { name = "pytest-django" }, - { name = "pytest-github-actions-annotate-failures" }, - { name = "pytest-httpserver" }, - { name = "pytest-sugar" }, - { name = "pytest-timeout" }, - { name = "recommonmark" }, - { name = "requests-tracker" }, - { name = "ruff" }, - { name = "sphinx" }, - { name = "sphinx-autodoc2" }, - { name = "sphinx-rtd-theme" }, - { name = "sphinxcontrib-mermaid" }, - { name = "ty" }, - { name = "uv" }, - { name = "viztracer" }, + { name = "bottle", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "bumpver", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "coverage", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-debug-toolbar", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "djdt-flamegraph", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "ipdb", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "linkify-it-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "logfire", extra = ["django"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "myst-parser", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-instrumentation-django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-instrumentation-sqlite3", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "prek", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyright", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest-codeblocks", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest-cov", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest-django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest-github-actions-annotate-failures", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest-httpserver", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest-sugar", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest-timeout", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "recommonmark", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests-tracker", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "ruff", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinx-autodoc2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinx-rtd-theme", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinxcontrib-mermaid", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "ty", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "uv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "viztracer", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] [package.metadata] @@ -353,14 +353,14 @@ name = "autobahn" version = "26.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cbor2" }, - { name = "cffi" }, - { name = "cryptography" }, - { name = "hyperlink" }, - { name = "msgpack", marker = "platform_python_implementation == 'CPython'" }, - { name = "txaio" }, - { name = "u-msgpack-python", marker = "platform_python_implementation != 'CPython'" }, - { name = "ujson" }, + { name = "cbor2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "cffi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "hyperlink", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "msgpack", marker = "(platform_python_implementation == 'CPython' and sys_platform == 'darwin') or (platform_python_implementation == 'CPython' and sys_platform == 'linux')" }, + { name = "txaio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "u-msgpack-python", marker = "(platform_python_implementation != 'CPython' and sys_platform == 'darwin') or (platform_python_implementation != 'CPython' and sys_platform == 'linux')" }, + { name = "ujson", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/73/f109f563c27e048e45d135d81af19e6ca391e24905550b06bd1c9d674c57/autobahn-26.7.1.tar.gz", hash = "sha256:c6949a2c6eb95fb1c218837dbda0a59abbbebafb8b11098551c01a7061dfd245", size = 14056542, upload-time = "2026-07-15T19:14:01.246Z" } wheels = [ @@ -408,7 +408,7 @@ name = "bleach" version = "6.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "webencodings" }, + { name = "webencodings", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/48/3c/e12ac860709702bd5ebeb9b56a4fe334f1001246ee1b8f2b7ee28912df7d/bleach-6.4.0.tar.gz", hash = "sha256:4202482733d85cedd04e59fcb2f89f4e4c7c385a78d3c3c23c30446843a37452", size = 204857, upload-time = "2026-06-05T13:01:13.734Z" } wheels = [ @@ -429,10 +429,10 @@ name = "bumpver" version = "2026.1132" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "colorama" }, - { name = "lexid" }, - { name = "toml" }, + { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "colorama", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "lexid", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "toml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/22/c0c9bf9e7ad6877e4c7c3dedd464e390d9fadb8927c61d6d77547eb17539/bumpver-2026.1132.tar.gz", hash = "sha256:80b223c23fca9bc9dd569b7a44680949d34bee23a738860d9a9b36f1abe3b0e0", size = 116784, upload-time = "2026-05-22T18:40:32.333Z" } wheels = [ @@ -476,7 +476,7 @@ name = "cffi" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "(implementation_name != 'PyPy' and sys_platform == 'darwin') or (implementation_name != 'PyPy' and sys_platform == 'linux')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } wheels = [ @@ -654,7 +654,7 @@ name = "croniter" version = "6.2.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-dateutil" }, + { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/37/57/2e2a65aee2a70483cb28e2b7e15a072d00a523207593b44400d4717bb100/croniter-6.2.4.tar.gz", hash = "sha256:fc124f751b1b04805c2a04b061898b436b45ab2320b045e1e052ea895de65189", size = 166267, upload-time = "2026-07-10T09:52:59.955Z" } wheels = [ @@ -666,7 +666,7 @@ name = "cryptography" version = "49.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "cffi", marker = "(platform_python_implementation != 'PyPy' and sys_platform == 'darwin') or (platform_python_implementation != 'PyPy' and sys_platform == 'linux')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } wheels = [ @@ -713,9 +713,9 @@ name = "daphne" version = "4.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asgiref" }, - { name = "autobahn" }, - { name = "twisted", extra = ["tls"] }, + { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "autobahn", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "twisted", extra = ["tls"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/64/d3/65ff32c01cc64d44441b038dbb7cfb0c6a5507a1c937b3d41bd99af7bdc4/daphne-4.2.2.tar.gz", hash = "sha256:6c3527d4ce32630ae054dfb0ef5578e9a35d2f39f0ebcd02ef4f9129a121ce8d", size = 47601, upload-time = "2026-06-03T10:53:13.31Z" } wheels = [ @@ -727,10 +727,10 @@ name = "dateparser" version = "1.4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "regex" }, - { name = "tzlocal" }, + { name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytz", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tzlocal", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d3/f4/561c49bca97af561d34eed27e3e831135eb5cb88e754c1150be41820f5c6/dateparser-1.4.1.tar.gz", hash = "sha256:f265df13c0380e2e07543ba74b67c0681aaa1096981ffcd35227e1aa0cb81c7c", size = 314734, upload-time = "2026-06-15T08:45:47.659Z" } wheels = [ @@ -751,8 +751,8 @@ name = "django" version = "6.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asgiref" }, - { name = "sqlparse" }, + { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/89/55/664f24ff81c9ea19cb7dfc851afeae1f3c2390c7aee01d4ded68b5c1580d/django-6.0.7.tar.gz", hash = "sha256:2998503fc083124fb58037084bfa00de323c7c743f05f1b4284e77bff0ab8890", size = 10921299, upload-time = "2026-07-07T13:51:26.485Z" } wheels = [ @@ -764,8 +764,8 @@ name = "django-admin-data-views" version = "0.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django" }, - { name = "django-settings-holder" }, + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-settings-holder", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/3f/2377a4088c0bf7ce677bb0a526cbf898a11e5528941d7cda303efef3bd73/django_admin_data_views-0.4.3.tar.gz", hash = "sha256:bd287a5d874febd8b544f83b47d0846fbf7b3e00a7f6633912630053c7ae4298", size = 12519, upload-time = "2024-11-24T14:18:00.406Z" } wheels = [ @@ -777,8 +777,8 @@ name = "django-auth-ldap" version = "5.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django" }, - { name = "python-ldap" }, + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "python-ldap", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/6d/d3ceb4b49e7153811a4b2d92bbe198a5ef2e2820469add3d6dc129ef2fab/django_auth_ldap-5.3.0.tar.gz", hash = "sha256:743d8107b146240b46f7e97207dc06cb11facc0cd70dce490b7ca09dd5643d19", size = 55272, upload-time = "2025-12-26T15:00:14.272Z" } wheels = [ @@ -790,8 +790,8 @@ name = "django-autotyping" version = "0.5.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django" }, - { name = "libcst" }, + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "libcst", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7b/d4/65d2b1c54f35116bd2d31d1064c523ded729353633389ecfc283a93b4c47/django_autotyping-0.5.1.tar.gz", hash = "sha256:b48c57d3d358a608109dd47698e64466e596983e8729bff130669dd744588c25", size = 78974, upload-time = "2024-05-29T14:48:28.561Z" } wheels = [ @@ -803,8 +803,8 @@ name = "django-debug-toolbar" version = "7.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django" }, - { name = "sqlparse" }, + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1e/ac/3ac674f99c64bfbcd9ae3d7f5f3577f23ea52884e5ac36842e1f024dce03/django_debug_toolbar-7.0.0.tar.gz", hash = "sha256:ef7494c5b459c149e87cc2da88d86e944064945e86bd7b2d879093586b5fefbf", size = 359560, upload-time = "2026-06-19T00:21:23.346Z" } wheels = [ @@ -816,7 +816,7 @@ name = "django-extensions" version = "4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django" }, + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6d/b3/ed0f54ed706ec0b54fd251cc0364a249c6cd6c6ec97f04dc34be5e929eac/django_extensions-4.1.tar.gz", hash = "sha256:7b70a4d28e9b840f44694e3f7feb54f55d495f8b3fa6c5c0e5e12bcb2aa3cdeb", size = 283078, upload-time = "2025-04-11T01:15:39.617Z" } wheels = [ @@ -828,8 +828,8 @@ name = "django-ninja" version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django" }, - { name = "pydantic" }, + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/7c/3307e17b872f545c88314b2737a22f965785dfb5a120d739b0131d0492c3/django_ninja-1.6.2.tar.gz", hash = "sha256:d56ae5aa4791068ef4ac9a66cfdf2fc11f507413ded35abb79c51d0d52ad6412", size = 3685599, upload-time = "2026-03-18T20:06:47.284Z" } wheels = [ @@ -850,7 +850,7 @@ name = "django-settings-holder" version = "0.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django" }, + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5f/1d/810c15a408987262bf70fc1892d4d5c3b2c1ae62e8715b73592c8f46ed37/django_settings_holder-0.3.0.tar.gz", hash = "sha256:d41eb6d6023d61c08e395f2406fd6f047d1edff2f0346d04323f3681f12372ef", size = 8580, upload-time = "2025-04-27T13:15:42.069Z" } wheels = [ @@ -862,11 +862,11 @@ name = "django-signal-webhooks" version = "0.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asgiref" }, - { name = "cryptography" }, - { name = "django" }, - { name = "django-settings-holder" }, - { name = "httpx" }, + { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-settings-holder", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/15/865e72e1da78bc6c6865ff16b0dffb11db62999fc91bed8c3c1668eac4c1/django_signal_webhooks-0.3.1.tar.gz", hash = "sha256:23dc439be2fdea24b746726495eb1a7a59440809056482eebceb153d050a3f5b", size = 17806, upload-time = "2024-10-31T23:34:40.37Z" } wheels = [ @@ -878,10 +878,10 @@ name = "django-stubs" version = "6.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django" }, - { name = "django-stubs-ext" }, - { name = "types-pyyaml" }, - { name = "typing-extensions" }, + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "django-stubs-ext", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "types-pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8a/90/087c6e424e705e05182e543ef6b366a59eb5c92ab008b0dbbba55f357a40/django_stubs-6.0.7.tar.gz", hash = "sha256:bc55431c0af745a64e39cf33a8d36c87dccbedeae2fe26fab47dd355270e8538", size = 282293, upload-time = "2026-07-14T10:08:27.122Z" } wheels = [ @@ -893,8 +893,8 @@ name = "django-stubs-ext" version = "6.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django" }, - { name = "typing-extensions" }, + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/50/917f7224ea470e89cdcdc93d3dfe75b8391adf976cf12f2ecdb5f5d122be/django_stubs_ext-6.0.7.tar.gz", hash = "sha256:c3172c5126614fd2a44d0196b313b44c21f717cb09477ba52b447d41f4ce613e", size = 6665, upload-time = "2026-07-14T10:07:56.933Z" } wheels = [ @@ -933,8 +933,8 @@ name = "email-validator" version = "2.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "dnspython" }, - { name = "idna" }, + { name = "dnspython", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } wheels = [ @@ -955,7 +955,7 @@ name = "googleapis-common-protos" version = "1.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "protobuf" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b5/c8/f439cffde755cffa462bfbb156278fa6f9d09119719af9814b858fd4f81f/googleapis_common_protos-1.75.0.tar.gz", hash = "sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd", size = 151035, upload-time = "2026-05-07T08:04:49.423Z" } wheels = [ @@ -976,8 +976,8 @@ name = "httpcore" version = "1.0.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "h11" }, + { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "h11", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } wheels = [ @@ -989,10 +989,10 @@ name = "httpx" version = "0.28.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, + { name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "httpcore", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } wheels = [ @@ -1004,7 +1004,7 @@ name = "hyperlink" version = "21.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "idna" }, + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } wheels = [ @@ -1034,7 +1034,7 @@ name = "incremental" version = "24.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "packaging" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/3c/82e84109e02c492f382c711c58a3dd91badda6d746def81a1465f74dc9f5/incremental-24.11.0.tar.gz", hash = "sha256:87d3480dbb083c1d736222511a8cf380012a8176c2456d01ef483242abbbcf8c", size = 24000, upload-time = "2025-11-28T02:30:17.861Z" } wheels = [ @@ -1055,8 +1055,8 @@ name = "ipdb" version = "0.13.13" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "decorator" }, - { name = "ipython" }, + { name = "decorator", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "ipython", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/1b/7e07e7b752017f7693a0f4d41c13e5ca29ce8cbcfdcc1fd6c4ad8c0a27a0/ipdb-0.13.13.tar.gz", hash = "sha256:e3ac6018ef05126d442af680aad863006ec19d02290561ac88b8b1c0b0cfc726", size = 17042, upload-time = "2023-03-09T15:40:57.487Z" } wheels = [ @@ -1068,16 +1068,16 @@ name = "ipython" version = "9.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "decorator" }, - { name = "ipython-pygments-lexers" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect" }, - { name = "prompt-toolkit" }, - { name = "psutil" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, + { name = "decorator", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "ipython-pygments-lexers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jedi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "matplotlib-inline", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pexpect", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "prompt-toolkit", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "psutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "stack-data", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" } wheels = [ @@ -1089,7 +1089,7 @@ name = "ipython-pygments-lexers" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pygments" }, + { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } wheels = [ @@ -1101,9 +1101,9 @@ name = "jambo" version = "0.1.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "email-validator" }, - { name = "jsonschema" }, - { name = "pydantic" }, + { name = "email-validator", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jsonschema", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/91/f5/74de157c7aece6a070f99f18201a0e2f46cdfd0f9e337efd411745ed9b22/jambo-0.1.7.tar.gz", hash = "sha256:df89ab8209ebdf7a6e92252ec925979cd3d32811bf4a8182a97dc35b7df58f74", size = 137822, upload-time = "2026-01-14T19:17:30.302Z" } @@ -1112,7 +1112,7 @@ name = "jedi" version = "0.20.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "parso" }, + { name = "parso", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/b7/a3635f6a2d7cf5b5dd98064fc1d5fbbafcb25477bcea204a3a92145d158b/jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011", size = 3119416, upload-time = "2026-05-01T23:38:47.814Z" } wheels = [ @@ -1124,7 +1124,7 @@ name = "jinja2" version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe" }, + { name = "markupsafe", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ @@ -1136,10 +1136,10 @@ name = "jsonschema" version = "4.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, + { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jsonschema-specifications", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "referencing", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "rpds-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } wheels = [ @@ -1151,7 +1151,7 @@ name = "jsonschema-specifications" version = "2025.9.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "referencing" }, + { name = "referencing", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } wheels = [ @@ -1172,8 +1172,8 @@ name = "libcst" version = "1.8.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyyaml", marker = "python_full_version >= '3.14'" }, - { name = "pyyaml-ft", marker = "python_full_version < '3.14'" }, + { name = "pyyaml", marker = "(python_full_version >= '3.14' and sys_platform == 'darwin') or (python_full_version >= '3.14' and sys_platform == 'linux')" }, + { name = "pyyaml-ft", marker = "(python_full_version < '3.14' and sys_platform == 'darwin') or (python_full_version < '3.14' and sys_platform == 'linux')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/cd/337df968b38d94c5aabd3e1b10630f047a2b345f6e1d4456bd9fe7417537/libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b", size = 891354, upload-time = "2025-11-03T22:33:30.621Z" } wheels = [ @@ -1208,7 +1208,7 @@ name = "linkify-it-py" version = "2.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "uc-micro-py" }, + { name = "uc-micro-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } wheels = [ @@ -1220,13 +1220,13 @@ name = "logfire" version = "4.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "executing" }, - { name = "opentelemetry-exporter-otlp-proto-http" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-sdk" }, - { name = "protobuf" }, - { name = "rich" }, - { name = "typing-extensions" }, + { name = "executing", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-exporter-otlp-proto-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c2/f2/34b8ebbd6bbd82c71055d6b881b24d8ada79a0e6692d3dd8cca5e86fadb3/logfire-4.37.0.tar.gz", hash = "sha256:7ee0cb64b59c356a41a1701fb84597037f8db1fa15df7a3715ef363e5a1de06a", size = 1212176, upload-time = "2026-06-12T20:47:06.904Z" } wheels = [ @@ -1235,8 +1235,8 @@ wheels = [ [package.optional-dependencies] django = [ - { name = "opentelemetry-instrumentation-asgi" }, - { name = "opentelemetry-instrumentation-django" }, + { name = "opentelemetry-instrumentation-asgi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-instrumentation-django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] [[package]] @@ -1244,7 +1244,7 @@ name = "markdown-it-py" version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "mdurl" }, + { name = "mdurl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ @@ -1296,7 +1296,7 @@ name = "matplotlib-inline" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "traitlets" }, + { name = "traitlets", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/c0/9f7c9a46090390368a4d7bcb76bb87a4a36c421e4c0792cdb53486ffac7a/matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79", size = 8150, upload-time = "2026-05-08T17:33:33.49Z" } wheels = [ @@ -1308,7 +1308,7 @@ name = "mdit-py-plugins" version = "0.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py" }, + { name = "markdown-it-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/59/fc/f8d0863f8862f25602c0404d75568e89fb6b4109804645e5cdfb1be5cf56/mdit_py_plugins-0.6.1.tar.gz", hash = "sha256:a2bca0f039f39dbd35fb74ae1b5f998608c437463371f0ff7f49a19a17a114d0", size = 56114, upload-time = "2026-05-13T09:03:38.91Z" } wheels = [ @@ -1361,12 +1361,12 @@ name = "myst-parser" version = "5.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils" }, - { name = "jinja2" }, - { name = "markdown-it-py" }, - { name = "mdit-py-plugins" }, - { name = "pyyaml" }, - { name = "sphinx" }, + { name = "docutils", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "markdown-it-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "mdit-py-plugins", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } wheels = [ @@ -1396,7 +1396,7 @@ name = "opentelemetry-api" version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/1c/125e1c936c0873796771b7f04f6c93b9f1bf5d424cea90fda94a99f61da8/opentelemetry_api-1.42.1.tar.gz", hash = "sha256:56c63bea9f77b62856be8c47600474acad853b2924b99b1687c4cb6297166716", size = 72296, upload-time = "2026-05-21T16:32:49.335Z" } wheels = [ @@ -1408,7 +1408,7 @@ name = "opentelemetry-exporter-otlp-proto-common" version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-proto" }, + { name = "opentelemetry-proto", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0e/9c/216acfeaedadf2e1937f4373929b20f73197c5c4a2546d4f584b7fa63813/opentelemetry_exporter_otlp_proto_common-1.42.1.tar.gz", hash = "sha256:04f1f01fb597c4249dfcd7f8b861c902c2102369d376d9d346ff38de4469a2ee", size = 21433, upload-time = "2026-05-21T16:32:55.526Z" } wheels = [ @@ -1420,13 +1420,13 @@ name = "opentelemetry-exporter-otlp-proto-http" version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "googleapis-common-protos" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-exporter-otlp-proto-common" }, - { name = "opentelemetry-proto" }, - { name = "opentelemetry-sdk" }, - { name = "requests" }, - { name = "typing-extensions" }, + { name = "googleapis-common-protos", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-exporter-otlp-proto-common", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-proto", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-sdk", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/77/32/826bfa1d80ecea24f47808de03cd4a0d13c17ecc07712f45123f0f61e4ac/opentelemetry_exporter_otlp_proto_http-1.42.1.tar.gz", hash = "sha256:bf142a21035d7571ac3a09cb2e5639f49886f243972883cfe777ed3bf02b734d", size = 25406, upload-time = "2026-05-21T16:32:56.807Z" } wheels = [ @@ -1438,10 +1438,10 @@ name = "opentelemetry-instrumentation" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "packaging" }, - { name = "wrapt" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/6d/4de72d97ff54db1ed270c7a59c9b904b917c0ac7af429c086c388b824ddb/opentelemetry_instrumentation-0.63b1.tar.gz", hash = "sha256:32368d6ae52c8de20aa790a6ad86b10a76f09956092337ae37d675773990e541", size = 41081, upload-time = "2026-05-21T16:36:14.206Z" } wheels = [ @@ -1453,11 +1453,11 @@ name = "opentelemetry-instrumentation-asgi" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asgiref" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, + { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-util-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a0/b5/7ea3a9fd1b80e89786c14250bfaecf32a753c3fd08232690f4da8dc16e29/opentelemetry_instrumentation_asgi-0.63b1.tar.gz", hash = "sha256:267b422416d768f3c7f4054883b41d9c3a7c943d86d20032b738c99a3dbb5862", size = 26151, upload-time = "2026-05-21T16:36:18.368Z" } wheels = [ @@ -1469,10 +1469,10 @@ name = "opentelemetry-instrumentation-dbapi" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "wrapt" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "wrapt", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/47/bf/2bb8048a3ba5686bb70e5d3d7dd2aa2b3d838ccd324e88557780f86b7635/opentelemetry_instrumentation_dbapi-0.63b1.tar.gz", hash = "sha256:406978ed56bcfc5fd246fd918e6b36d0f5de26fa396c78cf63326a7b530597c8", size = 19323, upload-time = "2026-05-21T16:36:27.036Z" } wheels = [ @@ -1484,11 +1484,11 @@ name = "opentelemetry-instrumentation-django" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-wsgi" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-instrumentation-wsgi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-util-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f7/37/90fd5f0dc4a2042dfdb609a9dbe42fde26b3690f9c33ece529b024251015/opentelemetry_instrumentation_django-0.63b1.tar.gz", hash = "sha256:f2071d2f92e4779c5a14dd452b0dfe426343599e6efa9d888304fb639a9f3101", size = 25565, upload-time = "2026-05-21T16:36:27.727Z" } wheels = [ @@ -1500,9 +1500,9 @@ name = "opentelemetry-instrumentation-sqlite3" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-instrumentation-dbapi" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-instrumentation-dbapi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/9c/b741eb1d9c6c74680913d0a4fd233c518e8f3f69a200a176121db795ccca/opentelemetry_instrumentation_sqlite3-0.63b1.tar.gz", hash = "sha256:3d61afda8358dc32135fabd27e5934bd25ea0eed68d64c34508f24ba8d723efc", size = 8420, upload-time = "2026-05-21T16:36:47.889Z" } wheels = [ @@ -1514,10 +1514,10 @@ name = "opentelemetry-instrumentation-wsgi" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-instrumentation" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "opentelemetry-util-http" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-instrumentation", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-util-http", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/55/832f287fb153adc25c05bc2594d00ac4d1dbeca8b19388b2666d5154c912/opentelemetry_instrumentation_wsgi-0.63b1.tar.gz", hash = "sha256:03d61c4678ce82402e7f37b6a3dbd84cb97b85b3cb416a78c2e74c7c6d9451fa", size = 19667, upload-time = "2026-05-21T16:36:53.873Z" } wheels = [ @@ -1529,7 +1529,7 @@ name = "opentelemetry-proto" version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "protobuf" }, + { name = "protobuf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/55/63eac3e1089b768ba014091fdd2ae8a9a440c821ef5e2b786909c94c8836/opentelemetry_proto-1.42.1.tar.gz", hash = "sha256:c6a51e6b4f05ae63565f3a113217f3d2bfaec68f78c02d7a6c85f9010d1cfca6", size = 45839, upload-time = "2026-05-21T16:33:03.937Z" } wheels = [ @@ -1541,9 +1541,9 @@ name = "opentelemetry-sdk" version = "1.42.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/f7/b390bd9bfd703bf98a68fea1f27786c6872331fd617164a54b8a59bdc008/opentelemetry_sdk-1.42.1.tar.gz", hash = "sha256:8c834e8f8c9ba4171d4ec843d0cb8a67e4c7394d3f9e9297e582cbd9456ddbf7", size = 239262, upload-time = "2026-05-21T16:33:04.641Z" } wheels = [ @@ -1555,8 +1555,8 @@ name = "opentelemetry-semantic-conventions" version = "0.63b1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "opentelemetry-api" }, - { name = "typing-extensions" }, + { name = "opentelemetry-api", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/99/4d7dd6df64795951413ce6e815f8cf1eb191daf7196ae86574589643d5f3/opentelemetry_semantic_conventions-0.63b1.tar.gz", hash = "sha256:3daf963611334b365e98a57438183eb012d3bfb40b2d931a9af613476b8701a9", size = 148340, upload-time = "2026-05-21T16:33:05.455Z" } wheels = [ @@ -1595,7 +1595,7 @@ name = "pexpect" version = "4.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ptyprocess" }, + { name = "ptyprocess", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } wheels = [ @@ -1655,7 +1655,7 @@ name = "prompt-toolkit" version = "3.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "wcwidth" }, + { name = "wcwidth", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } wheels = [ @@ -1708,7 +1708,7 @@ wheels = [ [package.optional-dependencies] binary = [ - { name = "psycopg-binary", marker = "implementation_name != 'pypy'" }, + { name = "psycopg-binary", marker = "(implementation_name != 'pypy' and sys_platform == 'darwin') or (implementation_name != 'pypy' and sys_platform == 'linux')" }, ] [[package]] @@ -1779,7 +1779,7 @@ name = "pyasn1-modules" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyasn1" }, + { name = "pyasn1", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } wheels = [ @@ -1800,10 +1800,10 @@ name = "pydantic" version = "2.13.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, + { name = "annotated-types", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pydantic-core", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } wheels = [ @@ -1815,7 +1815,7 @@ name = "pydantic-core" version = "2.46.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } wheels = [ @@ -1862,9 +1862,9 @@ name = "pydantic-settings" version = "2.14.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, + { name = "pydantic", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-inspection", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } wheels = [ @@ -1876,7 +1876,7 @@ name = "pydot" version = "4.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyparsing" }, + { name = "pyparsing", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/35/b17cb89ff865484c6a20ef46bf9d95a5f07328292578de0b295f4a6beec2/pydot-4.0.1.tar.gz", hash = "sha256:c2148f681c4a33e08bf0e26a9e5f8e4099a82e0e2a068098f32ce86577364ad5", size = 162594, upload-time = "2025-06-17T20:09:56.454Z" } wheels = [ @@ -1897,7 +1897,7 @@ name = "pyopenssl" version = "26.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/74/b7/da07bae88f5a9506b4def6f2f4903cf4c3b8831e560dba8fa18ca08f758f/pyopenssl-26.3.0.tar.gz", hash = "sha256:589de7fae1c9ea670d18422ed00fc04da787bbde8e1454aea872aa57b49ad341", size = 182024, upload-time = "2026-06-12T20:28:07.458Z" } wheels = [ @@ -1918,8 +1918,8 @@ name = "pyright" version = "1.1.411" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, + { name = "nodeenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } wheels = [ @@ -1931,10 +1931,10 @@ name = "pytest" version = "8.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, + { name = "iniconfig", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pluggy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } wheels = [ @@ -1946,7 +1946,7 @@ name = "pytest-codeblocks" version = "0.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/3a/cb/5f40df7db75c0ac2555009287bb97b322f8d79f9f2bfdfb0bdb560834c28/pytest_codeblocks-0.18.0-py3-none-any.whl", hash = "sha256:3fe944dc505107421204c83e9232e0155eea1279b9425a10bee327079b272efb", size = 8009, upload-time = "2026-06-15T20:48:02.608Z" }, @@ -1957,9 +1957,9 @@ name = "pytest-cov" version = "7.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "coverage" }, - { name = "pluggy" }, - { name = "pytest" }, + { name = "coverage", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pluggy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ @@ -1971,7 +1971,7 @@ name = "pytest-django" version = "4.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/13/2b/db9a193df89e5660137f5428063bcc2ced7ad790003b26974adf5c5ceb3b/pytest_django-4.12.0.tar.gz", hash = "sha256:df94ec819a83c8979c8f6de13d9cdfbe76e8c21d39473cfe2b40c9fc9be3c758", size = 91156, upload-time = "2026-02-14T18:40:49.235Z" } wheels = [ @@ -1983,7 +1983,7 @@ name = "pytest-github-actions-annotate-failures" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/a0/bdb91581b03c41016c78e16b8ec36c34d8508206fcb30f1951c9cdff2e97/pytest_github_actions_annotate_failures-0.4.2.tar.gz", hash = "sha256:5dd18304512361788bc7b5c5c805db853f03f4950c6be09b088de6bab8e2e6c9", size = 12158, upload-time = "2026-06-19T15:59:17.445Z" } wheels = [ @@ -1995,7 +1995,7 @@ name = "pytest-httpserver" version = "1.1.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "werkzeug" }, + { name = "werkzeug", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/17/ad187f46998814014f7cda309de700b87c0eb4b2e111e18bc8c819be7116/pytest_httpserver-1.1.5.tar.gz", hash = "sha256:dc3d82e1fe00e491829d8939c549bf4bd9b39a260f87113c619b9d517c2f8ff1", size = 70974, upload-time = "2026-02-14T13:27:23.412Z" } wheels = [ @@ -2007,8 +2007,8 @@ name = "pytest-sugar" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest" }, - { name = "termcolor" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "termcolor", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/4e/60fed105549297ba1a700e1ea7b828044842ea27d72c898990510b79b0e2/pytest-sugar-1.1.1.tar.gz", hash = "sha256:73b8b65163ebf10f9f671efab9eed3d56f20d2ca68bda83fa64740a92c08f65d", size = 16533, upload-time = "2025-08-23T12:19:35.737Z" } wheels = [ @@ -2020,7 +2020,7 @@ name = "pytest-timeout" version = "2.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pytest" }, + { name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } wheels = [ @@ -2032,7 +2032,7 @@ name = "python-dateutil" version = "2.9.0.post0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, + { name = "six", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ @@ -2053,8 +2053,8 @@ name = "python-ldap" version = "3.4.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyasn1" }, - { name = "pyasn1-modules" }, + { name = "pyasn1", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyasn1-modules", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b2/f4/60edeb794bbc9ed0ff2149bbaeec605f3ed331766459d195832ecbd0ba2d/python_ldap-3.4.7.tar.gz", hash = "sha256:bacd9fb680d20263d8570ade1cf234d90d281149a8beb4f079dd8f33f7613dc8", size = 387477, upload-time = "2026-05-20T13:41:04.358Z" } @@ -2069,7 +2069,7 @@ wheels = [ [package.optional-dependencies] diagrams = [ - { name = "pydot" }, + { name = "pydot", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] [[package]] @@ -2137,9 +2137,9 @@ name = "recommonmark" version = "0.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "commonmark" }, - { name = "docutils" }, - { name = "sphinx" }, + { name = "commonmark", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "docutils", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1c/00/3dd2bdc4184b0ce754b5b446325abf45c2e0a347e022292ddc44670f628c/recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67", size = 34444, upload-time = "2020-12-17T19:24:56.523Z" } wheels = [ @@ -2151,8 +2151,8 @@ name = "referencing" version = "0.37.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, + { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "rpds-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } wheels = [ @@ -2224,10 +2224,10 @@ name = "requests" version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, + { name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "charset-normalizer", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ @@ -2239,8 +2239,8 @@ name = "requests-tracker" version = "0.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "django" }, - { name = "sqlparse" }, + { name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e9/10/35d214c4eaa479251ebb6f774842e476cd4162ca939e72bb1d943131fb2c/requests_tracker-0.3.3.tar.gz", hash = "sha256:eb288d69ebcae49149b41d603960d101d7eb892627e3455a456fa1f9441d2a49", size = 49168, upload-time = "2023-11-04T01:24:11.992Z" } wheels = [ @@ -2252,8 +2252,8 @@ name = "rich" version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, + { name = "markdown-it-py", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ @@ -2265,8 +2265,8 @@ name = "rich-click" version = "1.9.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "rich" }, + { name = "click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f7/ea/21e4867ea0ef881ffd4c0550fc21a061435e50d6324bcd034396633cbc18/rich_click-1.9.8.tar.gz", hash = "sha256:4008f921da88b5d91646c134ec881c1500e5a6b3f093e90e8f29400e09608371", size = 75363, upload-time = "2026-05-28T19:54:59.144Z" } wheels = [ @@ -2377,8 +2377,8 @@ name = "service-identity" version = "26.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "cryptography" }, + { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "cryptography", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/87/ad52e2c582c0f0e7f0a1b86950494c38d67422dc0f5ed9044a5fb9569a49/service_identity-26.1.0.tar.gz", hash = "sha256:6358c52882c96e66ac4a55eb3a72c7dd4a70763f8cc6fa4e70abde2656f4bf3b", size = 42898, upload-time = "2026-05-30T12:04:55.184Z" } wheels = [ @@ -2426,22 +2426,22 @@ name = "sphinx" version = "9.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "roman-numerals" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, + { name = "alabaster", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "babel", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "docutils", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "imagesize", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "packaging", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "requests", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "roman-numerals", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "snowballstemmer", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinxcontrib-applehelp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinxcontrib-devhelp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinxcontrib-htmlhelp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinxcontrib-jsmath", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinxcontrib-qthelp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinxcontrib-serializinghtml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -2453,8 +2453,8 @@ name = "sphinx-autodoc2" version = "0.5.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "astroid" }, - { name = "typing-extensions" }, + { name = "astroid", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/17/5f/5350046d1aa1a56b063ae08b9ad871025335c9d55fe2372896ea48711da9/sphinx_autodoc2-0.5.0.tar.gz", hash = "sha256:7d76044aa81d6af74447080182b6868c7eb066874edc835e8ddf810735b6565a", size = 115077, upload-time = "2023-11-27T07:27:51.407Z" } wheels = [ @@ -2466,9 +2466,9 @@ name = "sphinx-rtd-theme" version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "docutils" }, - { name = "sphinx" }, - { name = "sphinxcontrib-jquery" }, + { name = "docutils", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinxcontrib-jquery", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } wheels = [ @@ -2507,7 +2507,7 @@ name = "sphinxcontrib-jquery" version = "4.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx" }, + { name = "sphinx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } wheels = [ @@ -2528,9 +2528,9 @@ name = "sphinxcontrib-mermaid" version = "2.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "jinja2" }, - { name = "pyyaml" }, - { name = "sphinx" }, + { name = "jinja2", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyyaml", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "sphinx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/51/29/54cf1f7e03630ca4859caba25bc192698954d7c630377c62d1e264715c37/sphinxcontrib_mermaid-2.0.3.tar.gz", hash = "sha256:a6865ef6b65b225c5403a3170de63a04a07227cada11a4a71a6b87b4f9ed185a", size = 20764, upload-time = "2026-07-08T00:30:44.216Z" } wheels = [ @@ -2569,9 +2569,9 @@ name = "stack-data" version = "0.6.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "asttokens" }, - { name = "executing" }, - { name = "pure-eval" }, + { name = "asttokens", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "executing", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pure-eval", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } wheels = [ @@ -2619,13 +2619,13 @@ name = "twisted" version = "26.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "automat" }, - { name = "constantly" }, - { name = "hyperlink" }, - { name = "incremental" }, - { name = "typing-extensions" }, - { name = "zope-interface" }, + { name = "attrs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "automat", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "constantly", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "hyperlink", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "incremental", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "zope-interface", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/97/6e9beb1e78247ae6dc34114f27d538cf2cb183c4afcd3609dfdf2b0439c8/twisted-26.4.0.tar.gz", hash = "sha256:dbfd0fe1ee409d0243fdd7a6a6ff14f4948cec1fd78e0376291f805e1501fae9", size = 3575095, upload-time = "2026-05-11T11:24:51.861Z" } wheels = [ @@ -2634,9 +2634,9 @@ wheels = [ [package.optional-dependencies] tls = [ - { name = "idna" }, - { name = "pyopenssl" }, - { name = "service-identity" }, + { name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "pyopenssl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "service-identity", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] [[package]] @@ -2693,7 +2693,7 @@ name = "typing-inspection" version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ @@ -2820,7 +2820,7 @@ name = "viztracer" version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "objprint" }, + { name = "objprint", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9c/ab/94ae463cd4e386f143e3520a274856c4f2b4858d7ae30aa223ae25e9a2e5/viztracer-1.1.1.tar.gz", hash = "sha256:dcd4b5ddcc3a40ee79a584406d984cb4d40bc3301a6c9015d8949d4445fe9346", size = 15667892, upload-time = "2025-11-11T00:03:17.751Z" } wheels = [ @@ -2874,7 +2874,7 @@ name = "werkzeug" version = "3.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "markupsafe" }, + { name = "markupsafe", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" } wheels = [ From 6a8062090bfb107f9c87004ca9db2b3f7afffdac Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 07:57:09 +0000 Subject: [PATCH 4/7] Slim DB helpers and address PR review feedback Helper reduction: - Move the Django DATABASES/SQLITE_CONNECTION_OPTIONS assembly into core/settings.py, dropping get_database_settings() and get_sqlite_connection_options() from misc/db.py. - Inline the single-use migration_table_columns() into its one migration. Review fixes: - search: match only scalar JSON *values* on postgres (jsonb_path_query over '$.**' scalar leaves), mirroring SQLite json_tree.atom so config keys no longer match. - CharField clamp now also runs in SnapshotQuerySet.bulk_create (bulk paths bypass the pre_save signal); truncate_overlong_charfields is dual-use. - Restore reverse-migration parity on postgres: crawls/machine/api initial migrations drop their rebuilt tables on reverse via drop_models_on_postgres. - docs: give DATABASE_NAME its own section so the anchor resolves correctly. - CI: only install postgres binaries on the shard that runs the postgres test. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019YHSjZM6TstSAMN2PhgfUg --- .github/workflows/test-parallel.yml | 1 + archivebox/api/migrations/0001_initial.py | 8 +- archivebox/core/models.py | 5 ++ archivebox/core/settings.py | 53 +++++++++-- archivebox/crawls/migrations/0001_initial.py | 8 +- archivebox/machine/migrations/0001_initial.py | 8 +- .../0011_remove_binary_output_dir.py | 17 ++-- archivebox/misc/db.py | 87 ++++++------------- archivebox/search/query.py | 16 +++- docs/Configuration.md | 9 +- 10 files changed, 126 insertions(+), 86 deletions(-) diff --git a/.github/workflows/test-parallel.yml b/.github/workflows/test-parallel.yml index 0f6d3403..ac997d2b 100644 --- a/.github/workflows/test-parallel.yml +++ b/.github/workflows/test-parallel.yml @@ -318,6 +318,7 @@ jobs: >> "$GITHUB_ENV" - name: Install PostgreSQL server binaries + if: contains(toJson(matrix.test.paths), 'test_postgres_backend') run: | set -Eeuo pipefail if ! ls /usr/lib/postgresql/*/bin/initdb >/dev/null 2>&1 && ! command -v initdb >/dev/null 2>&1; then diff --git a/archivebox/api/migrations/0001_initial.py b/archivebox/api/migrations/0001_initial.py index b3a2aab3..f2667e52 100644 --- a/archivebox/api/migrations/0001_initial.py +++ b/archivebox/api/migrations/0001_initial.py @@ -96,6 +96,12 @@ def _pg_sync_schema(apps, schema_editor): rebuild_models_from_migration_state(apps, schema_editor, "api", ["APIToken", "OutboundWebhook"]) +def _pg_drop_schema(apps, schema_editor): + from archivebox.misc.db import drop_models_on_postgres + + drop_models_on_postgres(apps, schema_editor, "api", ["OutboundWebhook", "APIToken"]) + + class Migration(migrations.Migration): initial = True @@ -271,5 +277,5 @@ class Migration(migrations.Migration): # On non-sqlite backends the raw DDL above is skipped, so the real # schema diverges from Django state. Rebuild both tables (including the # unique constraint) from the post-migration state (no-op on sqlite). - migrations.RunPython(_pg_sync_schema, reverse_code=migrations.RunPython.noop), + migrations.RunPython(_pg_sync_schema, reverse_code=_pg_drop_schema), ] diff --git a/archivebox/core/models.py b/archivebox/core/models.py index 04fd18e4..c710574a 100755 --- a/archivebox/core/models.py +++ b/archivebox/core/models.py @@ -183,9 +183,14 @@ class SnapshotQuerySet(models.QuerySet): for crawl_id, permissions in Crawl.objects.filter(pk__in=missing_crawl_ids).values_list("pk", "permissions") } + from archivebox.misc.db import truncate_overlong_charfields + for obj in objs: if isinstance(obj, self.model): obj.ensure_permissions_config(crawl_permissions=crawl_permissions_by_id.get(str(obj.crawl_id))) + # bulk_create bypasses pre_save, so clamp CharFields here too + # (e.g. page titles) to stay within postgres VARCHAR limits. + truncate_overlong_charfields(obj) return super().bulk_create(objs, *args, **kwargs) def paged_iterator(self, chunk_size: int = 500): diff --git a/archivebox/core/settings.py b/archivebox/core/settings.py index 80619bcd..7f5239bd 100644 --- a/archivebox/core/settings.py +++ b/archivebox/core/settings.py @@ -15,9 +15,9 @@ from archivebox.config.constants import CONSTANTS from archivebox.config.common import get_config from archivebox.core.routes_util import get_api_base_url, get_admin_base_url, get_base_url, normalize_base_url -# All sqlite-vs-postgres connection logic lives in archivebox.misc.db; -# DATABASE_ENGINE config selects the backend (sqlite by default). -from archivebox.misc.db import get_database_settings, get_sqlite_connection_options +# DATABASE_ENGINE config selects the backend (sqlite by default); the +# sqlite-vs-postgres helpers live in archivebox.misc.db. +from archivebox.misc.db import is_postgres, postgres_db_params from .settings_logging import SETTINGS_LOGGING @@ -220,11 +220,50 @@ DATABASE_NAME = CONFIG.DATABASE_NAME SQLITE_JOURNAL_MODE = CONFIG.SQLITE_JOURNAL_MODE SQLITE_MMAP_SIZE = CONFIG.SQLITE_MMAP_SIZE -SQLITE_CONNECTION_OPTIONS = get_sqlite_connection_options() - -DATABASES = { - "default": get_database_settings(), +SQLITE_CONNECTION_OPTIONS = { + "ENGINE": "archivebox.core.sqlite_backend", + "TIME_ZONE": CONSTANTS.TIMEZONE, + "OPTIONS": { + # https://gcollazo.com/optimal-sqlite-settings-for-django/ + # https://litestream.io/tips/#busy-timeout + # https://docs.djangoproject.com/en/5.1/ref/databases/#setting-pragma-options + "timeout": CONFIG.SQLITE_BUSY_TIMEOUT / 1000, + "check_same_thread": False, + # Keep SQLite on Django's default deferred transaction mode. BEGIN + # IMMEDIATE grabs the write lock as soon as atomic() opens, which is + # exactly what hurts ArchiveBox on large collections where Python code + # may do filesystem work before the actual row write. Deferred BEGIN + # keeps writes statement-scoped unless a caller explicitly opens a + # transaction around multiple writes. + "transaction_mode": None, + "init_command": ( + "PRAGMA foreign_keys=ON;" + f"PRAGMA busy_timeout = {CONFIG.SQLITE_BUSY_TIMEOUT};" + f"PRAGMA journal_mode = {SQLITE_JOURNAL_MODE};" + "PRAGMA synchronous = NORMAL;" + "PRAGMA temp_store = MEMORY;" + f"PRAGMA mmap_size = {SQLITE_MMAP_SIZE};" + "PRAGMA journal_size_limit = 67108864;" + "PRAGMA cache_size = 2000;" + ), + }, } + +if is_postgres(): + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + **postgres_db_params(), + "OPTIONS": {"connect_timeout": 10}, + }, + } +else: + DATABASES = { + "default": { + "NAME": DATABASE_NAME, + **SQLITE_CONNECTION_OPTIONS, + }, + } MIGRATION_MODULES = {"signal_webhooks": None} # Django requires DEFAULT_AUTO_FIELD to subclass AutoField (BigAutoField, SmallAutoField, etc.) diff --git a/archivebox/crawls/migrations/0001_initial.py b/archivebox/crawls/migrations/0001_initial.py index 6a079d30..f7e97fee 100644 --- a/archivebox/crawls/migrations/0001_initial.py +++ b/archivebox/crawls/migrations/0001_initial.py @@ -101,6 +101,12 @@ def _pg_sync_schema(apps, schema_editor): rebuild_models_from_migration_state(apps, schema_editor, "crawls", ["CrawlSchedule", "Crawl"]) +def _pg_drop_schema(apps, schema_editor): + from archivebox.misc.db import drop_models_on_postgres + + drop_models_on_postgres(apps, schema_editor, "crawls", ["Crawl", "CrawlSchedule"]) + + class Migration(migrations.Migration): initial = True @@ -210,5 +216,5 @@ class Migration(migrations.Migration): # schema diverges from Django state. Rebuild both tables from the # post-migration state (no-op on sqlite). Runs as a top-level op so # ``apps`` reflects the full state including the circular template FK. - migrations.RunPython(_pg_sync_schema, reverse_code=migrations.RunPython.noop), + migrations.RunPython(_pg_sync_schema, reverse_code=_pg_drop_schema), ] diff --git a/archivebox/machine/migrations/0001_initial.py b/archivebox/machine/migrations/0001_initial.py index cea929bb..7d688ea2 100644 --- a/archivebox/machine/migrations/0001_initial.py +++ b/archivebox/machine/migrations/0001_initial.py @@ -39,6 +39,12 @@ def _pg_sync_schema(apps, schema_editor): ) +def _pg_drop_schema(apps, schema_editor): + from archivebox.misc.db import drop_models_on_postgres + + drop_models_on_postgres(apps, schema_editor, "machine", ["Binary", "NetworkInterface", "Machine"]) + + _SQLITE_INITIAL_SQL = """ -- Create machine_machine table CREATE TABLE IF NOT EXISTS machine_machine ( @@ -279,5 +285,5 @@ class Migration(migrations.Migration): ), ], ), - migrations.RunPython(_pg_sync_schema, reverse_code=migrations.RunPython.noop), + migrations.RunPython(_pg_sync_schema, reverse_code=_pg_drop_schema), ] diff --git a/archivebox/machine/migrations/0011_remove_binary_output_dir.py b/archivebox/machine/migrations/0011_remove_binary_output_dir.py index 92f0e935..820637bf 100644 --- a/archivebox/machine/migrations/0011_remove_binary_output_dir.py +++ b/archivebox/machine/migrations/0011_remove_binary_output_dir.py @@ -2,17 +2,12 @@ from django.db import migrations def remove_output_dir_if_exists(apps, schema_editor): - if schema_editor.connection.vendor == "sqlite": - cursor = schema_editor.connection.cursor() - cursor.execute("PRAGMA table_info(machine_binary)") - columns = {row[1] for row in cursor.fetchall()} - else: - # Portable introspection: on non-sqlite the RemoveField below is - # state-only, so the real ALTER TABLE DROP COLUMN must run here too. - # A fresh non-sqlite DB has machine_binary with output_dir from 0001. - from archivebox.misc.db import migration_table_columns - - columns = migration_table_columns(schema_editor.connection, "machine_binary") + # On non-sqlite the RemoveField below is state-only, so the real + # ALTER TABLE DROP COLUMN must run here too (a fresh DB has machine_binary + # with output_dir from 0001). Portable introspection works on both backends. + connection = schema_editor.connection + with connection.cursor() as cursor: + columns = {col.name for col in connection.introspection.get_table_description(cursor, "machine_binary")} if "output_dir" not in columns: return diff --git a/archivebox/misc/db.py b/archivebox/misc/db.py index f154b8fe..8d30de0b 100644 --- a/archivebox/misc/db.py +++ b/archivebox/misc/db.py @@ -57,57 +57,6 @@ def postgres_db_params() -> dict[str, str]: } -def get_sqlite_connection_options() -> dict[str, Any]: - """ENGINE + OPTIONS for the sqlite backend (without NAME).""" - from archivebox.config.common import get_config - - config = get_config() - return { - "ENGINE": "archivebox.core.sqlite_backend", - "TIME_ZONE": CONSTANTS.TIMEZONE, - "OPTIONS": { - # https://gcollazo.com/optimal-sqlite-settings-for-django/ - # https://litestream.io/tips/#busy-timeout - # https://docs.djangoproject.com/en/5.1/ref/databases/#setting-pragma-options - "timeout": config.SQLITE_BUSY_TIMEOUT / 1000, - "check_same_thread": False, - # Keep SQLite on Django's default deferred transaction mode. BEGIN - # IMMEDIATE grabs the write lock as soon as atomic() opens, which is - # exactly what hurts ArchiveBox on large collections where Python code - # may do filesystem work before the actual row write. Deferred BEGIN - # keeps writes statement-scoped unless a caller explicitly opens a - # transaction around multiple writes. - "transaction_mode": None, - "init_command": ( - "PRAGMA foreign_keys=ON;" - f"PRAGMA busy_timeout = {config.SQLITE_BUSY_TIMEOUT};" - f"PRAGMA journal_mode = {config.SQLITE_JOURNAL_MODE};" - "PRAGMA synchronous = NORMAL;" - "PRAGMA temp_store = MEMORY;" - f"PRAGMA mmap_size = {config.SQLITE_MMAP_SIZE};" - "PRAGMA journal_size_limit = 67108864;" - "PRAGMA cache_size = 2000;" - ), - }, - } - - -def get_database_settings() -> dict[str, Any]: - """The DATABASES['default'] dict for Django settings, per configured backend.""" - from archivebox.config.common import get_config - - if is_postgres(): - return { - "ENGINE": "django.db.backends.postgresql", - **postgres_db_params(), - "OPTIONS": {"connect_timeout": 10}, - } - return { - "NAME": get_config().DATABASE_NAME, - **get_sqlite_connection_options(), - } - - def _psycopg_connect(dbname: str | None = None, connect_timeout: int = 5): import psycopg @@ -214,17 +163,23 @@ def approximate_row_counts(connection) -> dict[str, int]: return counts -def truncate_overlong_charfields(sender, instance, **kwargs) -> None: - """pre_save receiver: clamp CharField values to their declared max_length. +def truncate_overlong_charfields(instance=None, **kwargs) -> None: + """Clamp a model instance's CharField values to their declared max_length. SQLite never enforces VARCHAR(n) limits, so ArchiveBox has always stored overlong values (e.g. long crawl labels or page titles) untruncated. PostgreSQL enforces them and would raise DataError on save instead. - Truncating at save time keeps writes succeeding identically on both - backends. Registered once for all models in CoreConfig.ready(). + Truncating keeps writes succeeding identically on both backends. + + Dual-use: works as a ``pre_save`` receiver (Django passes ``instance=`` and + ``sender=`` as kwargs; registered in ``CoreConfig.ready()``) and as a plain + ``truncate_overlong_charfields(obj)`` call for ``bulk_create`` paths, which + bypass signals. """ from django.db import models as dj_models + if instance is None: + return for field in instance._meta.local_concrete_fields: if isinstance(field, dj_models.CharField) and field.max_length: value = getattr(instance, field.attname, None) @@ -235,12 +190,6 @@ def truncate_overlong_charfields(sender, instance, **kwargs) -> None: # --- migration helpers ------------------------------------------------------ -def migration_table_columns(connection, table_name: str) -> set[str]: - """Portable column-name introspection usable from inside migrations.""" - with connection.cursor() as cursor: - return {col.name for col in connection.introspection.get_table_description(cursor, table_name)} - - def rebuild_models_from_migration_state(apps, schema_editor, app_label: str, model_names: list[str]) -> None: """(non-sqlite only) Drop and recreate the given models' tables from the current migration state. @@ -264,6 +213,22 @@ def rebuild_models_from_migration_state(apps, schema_editor, app_label: str, mod schema_editor.create_model(model) +def drop_models_on_postgres(apps, schema_editor, app_label: str, model_names: list[str]) -> None: + """Reverse companion to ``rebuild_models_from_migration_state``. + + Drops the given models' tables on non-sqlite backends (in the order given, + so callers pass reverse-dependency order). No-op on sqlite, whose reverse is + handled by the gated ``RunSQL`` reverse_sql instead. + """ + if schema_editor.connection.vendor == "sqlite": + return + existing_tables = set(schema_editor.connection.introspection.table_names()) + for model_name in model_names: + model = apps.get_model(app_label, model_name) + if model._meta.db_table in existing_tables: + schema_editor.delete_model(model) + + def run_db_analyze_batch( remaining: list[str] | None, *, diff --git a/archivebox/search/query.py b/archivebox/search/query.py index 2142faa4..86eb7745 100644 --- a/archivebox/search/query.py +++ b/archivebox/search/query.py @@ -40,10 +40,20 @@ def crawl_config_values_search_wave(query: str) -> Q | None: params=[pattern], ) elif connection.vendor == "postgresql": - # jsonb has no json_tree() equivalent; matching the serialized jsonb text - # covers every nested value (keys can also match, which only widens recall). + # Match only scalar config *values*, not keys (mirrors SQLite's + # json_tree.atom). jsonb_path_query('$.**') walks every nested node; + # keep the non-container leaves and compare their text form. matching_crawls = Crawl.objects.extra( - where=["LOWER(config::text) LIKE %s ESCAPE '\\'"], + where=[ + """ + EXISTS ( + SELECT 1 + FROM jsonb_path_query(config, '$.**') AS leaf + WHERE jsonb_typeof(leaf) NOT IN ('object', 'array') + AND LOWER(leaf #>> '{}') LIKE %s ESCAPE '\\' + ) + """, + ], params=[pattern], ) else: diff --git a/docs/Configuration.md b/docs/Configuration.md index 5a996bba..8a78f06b 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -719,11 +719,18 @@ DATABASE_PASSWORD = s3cret **Possible Values:** [`127.0.0.1`] / [`5432`] / [`archivebox`] / [empty] PostgreSQL connection settings, used only when [`DATABASE_ENGINE`](#database_engine)`=postgres`. Settable as `ARCHIVEBOX_DATABASE_HOST`, `ARCHIVEBOX_DATABASE_PORT`, `ARCHIVEBOX_DATABASE_USER`, and `ARCHIVEBOX_DATABASE_PASSWORD`. -`DATABASE_HOST` may also be a path to a directory containing a PostgreSQL unix socket (e.g. `/var/run/postgresql`). When `DATABASE_ENGINE=postgres`, [`DATABASE_NAME`](#database_name) is the name of the PostgreSQL database (default: `archivebox`) instead of a file path. +`DATABASE_HOST` may also be a path to a directory containing a PostgreSQL unix socket (e.g. `/var/run/postgresql`). --- +#### `DATABASE_NAME` +**Possible Values:** [`index.sqlite3`] / `archivebox` / ... +The main index database. Settable as `ARCHIVEBOX_DATABASE_NAME`. + +With the default [`DATABASE_ENGINE`](#database_engine)`=sqlite`, this is the path to the SQLite index file inside the data directory (`index.sqlite3`). With `DATABASE_ENGINE=postgres`, it is the name of the PostgreSQL database instead (default: `archivebox`), created automatically by `archivebox init` if it does not exist. + +--- #### `SQLITE_JOURNAL_MODE` **Possible Values:** [`WAL`]/`DELETE`/`TRUNCATE`/`PERSIST`/`MEMORY`/`OFF` SQLite [journal mode](https://www.sqlite.org/pragma.html#pragma_journal_mode), applied via `PRAGMA journal_mode = ...` on every new connection. Settable as `ARCHIVEBOX_SQLITE_JOURNAL_MODE`. From ee6867e812c98a5812a325358e68a6aeaada1b2f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 10:28:04 +0000 Subject: [PATCH 5/7] Use is_postgres() in snapshot URL query (centralize backend check) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019YHSjZM6TstSAMN2PhgfUg --- archivebox/core/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/archivebox/core/views.py b/archivebox/core/views.py index c046354c..857d7170 100644 --- a/archivebox/core/views.py +++ b/archivebox/core/views.py @@ -16,7 +16,6 @@ from django.utils.safestring import mark_safe from django.views import View from django.views.generic.list import ListView from django.views.generic import FormView -from django.db import connection from django.db.models import Case, IntegerField, Q, Value, When from django.core.paginator import InvalidPage from django.contrib import messages @@ -277,8 +276,10 @@ class SnapshotView(View): """ def _fragmentless_url_query(url: str) -> Q: + from archivebox.misc.db import is_postgres + canonical = without_fragment(url) - if connection.vendor == "sqlite": + if not is_postgres(): # Use a range comparison (url >= 'canonical#' AND url < 'canonical#\U0010ffff') # instead of LIKE/__startswith — SQLite's case-insensitive LIKE bypasses the # url index and forces a full-table scan over ~1M rows (~250ms). The range From e6102b52ac42300cd1558259bf53d54048b0db2f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 10:32:59 +0000 Subject: [PATCH 6/7] Register new DATABASE_CONFIG docs example in codeblocks inventory The postgres ArchiveBox.conf example added to Configuration.md is an illustration snippet; add it to docs/codeblocks.toml so the docs manifest inventory check passes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019YHSjZM6TstSAMN2PhgfUg --- docs/codeblocks.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/codeblocks.toml b/docs/codeblocks.toml index 74e14ac9..4b702a08 100644 --- a/docs/codeblocks.toml +++ b/docs/codeblocks.toml @@ -92,6 +92,7 @@ version = 2 "7890998169008ad0-1" = "illustration" # docs/Configuration.md +"48b48da014bd91dd-1" = "illustration" "129eb9ca56f17500-1" = "illustration" "798793d9d6b2e12e-1" = "run" "3fecc495abdc20ef-1" = "illustration" From 2df28d9a724b8d7cfc7ea593ce3be1034fa3d461 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 10:33:43 +0000 Subject: [PATCH 7/7] Gate postgres install to the sharded test_postgres_backend job Re-apply the postgres-server install step in dev's new per-file sharded test-parallel structure, conditioned on the shard whose matrix.test.path is the postgres backend test so other shards skip the apt install. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019YHSjZM6TstSAMN2PhgfUg --- .github/workflows/test-parallel.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test-parallel.yml b/.github/workflows/test-parallel.yml index 29cdbb13..b344e630 100644 --- a/.github/workflows/test-parallel.yml +++ b/.github/workflows/test-parallel.yml @@ -120,6 +120,14 @@ jobs: fi uv sync --locked --dev "${extra_args[@]}" + - name: Install PostgreSQL server binaries + if: contains(matrix.test.path, 'test_postgres_backend') + run: | + set -Eeuo pipefail + if ! ls /usr/lib/postgresql/*/bin/initdb >/dev/null 2>&1 && ! command -v initdb >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y postgresql + fi + - name: Run ${{ matrix.test.name }} env: TEST_PATH: ${{ matrix.test.path }}