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 = [