opensanctions-pravda 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenSanctions
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: opensanctions-pravda
3
+ Version: 0.1.0
4
+ Summary: Durable web evidence capture with a remote browser, Postgres, and content-addressed storage
5
+ Keywords: evidence,web-archiving,web-capture,playwright,har,forensics
6
+ Author: OpenSanctions
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Dist: alembic>=1.18.5
18
+ Requires-Dist: asyncpg>=0.31.0
19
+ Requires-Dist: beautifulsoup4>=4.15.0
20
+ Requires-Dist: fsspec[gcs]>=2026.4.0
21
+ Requires-Dist: playwright>=1.60.0
22
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.50
23
+ Requires-Python: >=3.13
24
+ Project-URL: Homepage, https://github.com/opensanctions/pravda
25
+ Project-URL: Documentation, https://github.com/opensanctions/pravda#readme
26
+ Project-URL: Repository, https://github.com/opensanctions/pravda
27
+ Project-URL: Issues, https://github.com/opensanctions/pravda/issues
28
+ Description-Content-Type: text/markdown
29
+
30
+ # Pravda
31
+
32
+ Pravda is a Python library for durable web evidence capture. It drives a
33
+ remote Playwright browser to preserve rendered HTML, plaintext, full-page
34
+ screenshots, metadata, and HAR recordings with response bodies. Snapshots are
35
+ recorded in Postgres and on any fsspec-compatible backend for later inspection
36
+ or comparison.
37
+
38
+ Pravda is a **library, not a service**: it connects directly from the caller's
39
+ process to the browser, database, and storage backend. Applications own that
40
+ infrastructure (see [Infrastructure](#infrastructure)).
41
+
42
+ - **Python** 3.13+
43
+ - **Browser**: a remote Playwright Chromium WebSocket endpoint (headed Chrome
44
+ under xvfb). The browser is a client connection; Pravda does not launch one.
45
+ - **Database**: PostgreSQL, upgraded to Pravda's schema with the
46
+ [migration helper](#database-migrations).
47
+ - **Storage**: any fsspec URL (local path, `s3://`, `gs://`, …) for
48
+ content-addressed artifacts.
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ pip install opensanctions-pravda
54
+ ```
55
+
56
+ ## Quick start
57
+
58
+ Construct a [`PravdaConfig`](#configuration), build a long-lived `Pravda`, and
59
+ use it as an async context manager so its database engine is disposed on
60
+ teardown. Reuse a single instance across captures — it owns the pooled engine,
61
+ session factory, and storage backend, while each capture opens its own browser
62
+ connection.
63
+
64
+ ```python
65
+ from pravda import Pravda, PravdaConfig
66
+
67
+ config = PravdaConfig(
68
+ database_url="postgresql+asyncpg://pravda:pravda@localhost:5432/pravda",
69
+ browser_ws_url="ws://localhost:3000",
70
+ storage_base_path="./data",
71
+ )
72
+
73
+ async def capture_example():
74
+ async with Pravda(config) as pravda:
75
+ snapshot = await pravda.snapshot("https://example.com")
76
+ print(snapshot.id, snapshot.http_status, snapshot.rendered_html)
77
+ ```
78
+
79
+ ## Configuration
80
+
81
+ `PravdaConfig` takes three settings, supplied explicitly per instance:
82
+
83
+ - `database_url` — async SQLAlchemy Postgres URL
84
+ (`postgresql+asyncpg://user:pass@host/db`).
85
+ - `browser_ws_url` — remote Playwright WebSocket URL.
86
+ - `storage_base_path` — fsspec storage URL, such as `./data`, `s3://bucket`,
87
+ or `gs://bucket`.
88
+
89
+ ## Usage
90
+
91
+ ### Capture a page
92
+
93
+ By default, Pravda navigates to the URL, waits for the normal `load` state,
94
+ captures the evidence, and persists the result. The complete pipeline is
95
+ bounded by a wall-clock timeout:
96
+
97
+ ```python
98
+ async def capture_example():
99
+ async with Pravda(config) as pravda:
100
+ snapshot = await pravda.snapshot("https://example.com")
101
+ ```
102
+
103
+ For custom navigation or interaction, pass an async `drive(page, url)`
104
+ callback. The callback owns the initial navigation; Pravda owns capture,
105
+ persistence, and cleanup, and records the state it leaves behind.
106
+
107
+ ```python
108
+ async def drive(page, url):
109
+ await page.goto(url, wait_until="commit")
110
+ await page.wait_for_selector(".results")
111
+
112
+ async def capture_results():
113
+ async with Pravda(config) as pravda:
114
+ snapshot = await pravda.snapshot("https://example.com", drive=drive)
115
+ ```
116
+
117
+ `page` is a real `playwright.async_api.Page`, so selectors, clicks, form
118
+ fills, and other Playwright operations are available. Playwright errors and
119
+ timeouts from `drive` are persisted as failed snapshots. Other callback
120
+ exceptions propagate and persist nothing.
121
+
122
+ Chrome is configured to download PDFs instead of opening its viewer. In custom
123
+ callbacks, `page.goto()` may therefore raise `Download is starting`; catch
124
+ that Playwright error if the download is expected. Pravda recovers the
125
+ downloaded body into the HAR.
126
+
127
+ ### Query history
128
+
129
+ The configured instance returns all snapshots for an exact URL, newest first:
130
+
131
+ ```python
132
+ async def print_history():
133
+ async with Pravda(config) as pravda:
134
+ history = await pravda.snapshots("https://example.com")
135
+ for snapshot in history:
136
+ print(snapshot.captured_at, snapshot.http_status)
137
+ ```
138
+
139
+ ## Database migrations
140
+
141
+ Alembic owns the Pravda schema; the migration scripts ship inside the
142
+ distribution. Bring a database up to the current schema head from application
143
+ startup — the database URL is passed explicitly and **no** `DATABASE_URL`
144
+ environment variable is required:
145
+
146
+ ```python
147
+ import pravda
148
+
149
+ async def setup():
150
+ await pravda.migrate("postgresql+asyncpg://user:pass@host/db")
151
+ ```
152
+
153
+ `migrate()` runs the packaged revisions through Alembic (not
154
+ `metadata.create_all`), is safe to call repeatedly (a database already at head
155
+ is a no-op), works from inside a running event loop, and disposes the engine
156
+ it creates. Database and migration failures propagate. There is no downgrade
157
+ or automatic-startup behavior: call `migrate()` where and when you want the
158
+ schema applied.
159
+
160
+ ## Storage
161
+
162
+ Artifacts are content-addressed files under `Snapshot.prefix`. The `plaintext`,
163
+ `rendered_html`, and `screenshot` fields contain filenames; HAR
164
+ `response.content._file` fields refer to stored response bodies. Consumers
165
+ read these files directly from the shared fsspec backend.
166
+
167
+ ## Infrastructure
168
+
169
+ Applications own the external infrastructure Pravda talks to; Pravda does not
170
+ launch or manage it:
171
+
172
+ - **Browser** — a remote Playwright Chromium server exposed over WebSocket.
173
+ See the repository for a reference Docker image that runs headed Chrome under
174
+ xvfb and accepts launch options through the `x-playwright-launch-options`
175
+ WebSocket header.
176
+ - **Postgres** — a database the application provisions and
177
+ [migrates](#database-migrations).
178
+ - **Storage** — an fsspec backend the application points at via
179
+ `storage_base_path`.
180
+
181
+ ## Development
182
+
183
+ Requires [uv](https://docs.astral.sh/uv/) and Docker.
184
+
185
+ ```bash
186
+ # Start the remote browser and Postgres
187
+ docker compose up -d
188
+
189
+ # Install dependencies
190
+ uv sync
191
+
192
+ # Validate
193
+ uv run pytest
194
+ uv run ruff check .
195
+ uv run ruff format --check .
196
+ ```
197
+
198
+ The migration scripts live inside the package at `pravda/migrations`. After
199
+ changing models in `pravda/db.py`, the developer `alembic` command reads
200
+ `DATABASE_URL` and points at the packaged scripts via `alembic.ini`:
201
+
202
+ ```bash
203
+ DATABASE_URL=postgresql+asyncpg://pravda:pravda@localhost:5432/pravda \
204
+ uv run alembic upgrade head
205
+ DATABASE_URL=postgresql+asyncpg://pravda:pravda@localhost:5432/pravda \
206
+ uv run alembic revision --autogenerate -m "describe the change"
207
+ ```
208
+
209
+ ## License
210
+
211
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,182 @@
1
+ # Pravda
2
+
3
+ Pravda is a Python library for durable web evidence capture. It drives a
4
+ remote Playwright browser to preserve rendered HTML, plaintext, full-page
5
+ screenshots, metadata, and HAR recordings with response bodies. Snapshots are
6
+ recorded in Postgres and on any fsspec-compatible backend for later inspection
7
+ or comparison.
8
+
9
+ Pravda is a **library, not a service**: it connects directly from the caller's
10
+ process to the browser, database, and storage backend. Applications own that
11
+ infrastructure (see [Infrastructure](#infrastructure)).
12
+
13
+ - **Python** 3.13+
14
+ - **Browser**: a remote Playwright Chromium WebSocket endpoint (headed Chrome
15
+ under xvfb). The browser is a client connection; Pravda does not launch one.
16
+ - **Database**: PostgreSQL, upgraded to Pravda's schema with the
17
+ [migration helper](#database-migrations).
18
+ - **Storage**: any fsspec URL (local path, `s3://`, `gs://`, …) for
19
+ content-addressed artifacts.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ pip install opensanctions-pravda
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ Construct a [`PravdaConfig`](#configuration), build a long-lived `Pravda`, and
30
+ use it as an async context manager so its database engine is disposed on
31
+ teardown. Reuse a single instance across captures — it owns the pooled engine,
32
+ session factory, and storage backend, while each capture opens its own browser
33
+ connection.
34
+
35
+ ```python
36
+ from pravda import Pravda, PravdaConfig
37
+
38
+ config = PravdaConfig(
39
+ database_url="postgresql+asyncpg://pravda:pravda@localhost:5432/pravda",
40
+ browser_ws_url="ws://localhost:3000",
41
+ storage_base_path="./data",
42
+ )
43
+
44
+ async def capture_example():
45
+ async with Pravda(config) as pravda:
46
+ snapshot = await pravda.snapshot("https://example.com")
47
+ print(snapshot.id, snapshot.http_status, snapshot.rendered_html)
48
+ ```
49
+
50
+ ## Configuration
51
+
52
+ `PravdaConfig` takes three settings, supplied explicitly per instance:
53
+
54
+ - `database_url` — async SQLAlchemy Postgres URL
55
+ (`postgresql+asyncpg://user:pass@host/db`).
56
+ - `browser_ws_url` — remote Playwright WebSocket URL.
57
+ - `storage_base_path` — fsspec storage URL, such as `./data`, `s3://bucket`,
58
+ or `gs://bucket`.
59
+
60
+ ## Usage
61
+
62
+ ### Capture a page
63
+
64
+ By default, Pravda navigates to the URL, waits for the normal `load` state,
65
+ captures the evidence, and persists the result. The complete pipeline is
66
+ bounded by a wall-clock timeout:
67
+
68
+ ```python
69
+ async def capture_example():
70
+ async with Pravda(config) as pravda:
71
+ snapshot = await pravda.snapshot("https://example.com")
72
+ ```
73
+
74
+ For custom navigation or interaction, pass an async `drive(page, url)`
75
+ callback. The callback owns the initial navigation; Pravda owns capture,
76
+ persistence, and cleanup, and records the state it leaves behind.
77
+
78
+ ```python
79
+ async def drive(page, url):
80
+ await page.goto(url, wait_until="commit")
81
+ await page.wait_for_selector(".results")
82
+
83
+ async def capture_results():
84
+ async with Pravda(config) as pravda:
85
+ snapshot = await pravda.snapshot("https://example.com", drive=drive)
86
+ ```
87
+
88
+ `page` is a real `playwright.async_api.Page`, so selectors, clicks, form
89
+ fills, and other Playwright operations are available. Playwright errors and
90
+ timeouts from `drive` are persisted as failed snapshots. Other callback
91
+ exceptions propagate and persist nothing.
92
+
93
+ Chrome is configured to download PDFs instead of opening its viewer. In custom
94
+ callbacks, `page.goto()` may therefore raise `Download is starting`; catch
95
+ that Playwright error if the download is expected. Pravda recovers the
96
+ downloaded body into the HAR.
97
+
98
+ ### Query history
99
+
100
+ The configured instance returns all snapshots for an exact URL, newest first:
101
+
102
+ ```python
103
+ async def print_history():
104
+ async with Pravda(config) as pravda:
105
+ history = await pravda.snapshots("https://example.com")
106
+ for snapshot in history:
107
+ print(snapshot.captured_at, snapshot.http_status)
108
+ ```
109
+
110
+ ## Database migrations
111
+
112
+ Alembic owns the Pravda schema; the migration scripts ship inside the
113
+ distribution. Bring a database up to the current schema head from application
114
+ startup — the database URL is passed explicitly and **no** `DATABASE_URL`
115
+ environment variable is required:
116
+
117
+ ```python
118
+ import pravda
119
+
120
+ async def setup():
121
+ await pravda.migrate("postgresql+asyncpg://user:pass@host/db")
122
+ ```
123
+
124
+ `migrate()` runs the packaged revisions through Alembic (not
125
+ `metadata.create_all`), is safe to call repeatedly (a database already at head
126
+ is a no-op), works from inside a running event loop, and disposes the engine
127
+ it creates. Database and migration failures propagate. There is no downgrade
128
+ or automatic-startup behavior: call `migrate()` where and when you want the
129
+ schema applied.
130
+
131
+ ## Storage
132
+
133
+ Artifacts are content-addressed files under `Snapshot.prefix`. The `plaintext`,
134
+ `rendered_html`, and `screenshot` fields contain filenames; HAR
135
+ `response.content._file` fields refer to stored response bodies. Consumers
136
+ read these files directly from the shared fsspec backend.
137
+
138
+ ## Infrastructure
139
+
140
+ Applications own the external infrastructure Pravda talks to; Pravda does not
141
+ launch or manage it:
142
+
143
+ - **Browser** — a remote Playwright Chromium server exposed over WebSocket.
144
+ See the repository for a reference Docker image that runs headed Chrome under
145
+ xvfb and accepts launch options through the `x-playwright-launch-options`
146
+ WebSocket header.
147
+ - **Postgres** — a database the application provisions and
148
+ [migrates](#database-migrations).
149
+ - **Storage** — an fsspec backend the application points at via
150
+ `storage_base_path`.
151
+
152
+ ## Development
153
+
154
+ Requires [uv](https://docs.astral.sh/uv/) and Docker.
155
+
156
+ ```bash
157
+ # Start the remote browser and Postgres
158
+ docker compose up -d
159
+
160
+ # Install dependencies
161
+ uv sync
162
+
163
+ # Validate
164
+ uv run pytest
165
+ uv run ruff check .
166
+ uv run ruff format --check .
167
+ ```
168
+
169
+ The migration scripts live inside the package at `pravda/migrations`. After
170
+ changing models in `pravda/db.py`, the developer `alembic` command reads
171
+ `DATABASE_URL` and points at the packaged scripts via `alembic.ini`:
172
+
173
+ ```bash
174
+ DATABASE_URL=postgresql+asyncpg://pravda:pravda@localhost:5432/pravda \
175
+ uv run alembic upgrade head
176
+ DATABASE_URL=postgresql+asyncpg://pravda:pravda@localhost:5432/pravda \
177
+ uv run alembic revision --autogenerate -m "describe the change"
178
+ ```
179
+
180
+ ## License
181
+
182
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,67 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11.26,<0.12.0"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ name = "opensanctions-pravda"
7
+ version = "0.1.0"
8
+ description = "Durable web evidence capture with a remote browser, Postgres, and content-addressed storage"
9
+ readme = "README.md"
10
+ requires-python = ">=3.13"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "OpenSanctions" }]
14
+ keywords = [
15
+ "evidence",
16
+ "web-archiving",
17
+ "web-capture",
18
+ "playwright",
19
+ "har",
20
+ "forensics",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 4 - Beta",
24
+ "Intended Audience :: Developers",
25
+ "Operating System :: OS Independent",
26
+ "Programming Language :: Python :: 3",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Programming Language :: Python :: 3.14",
29
+ "Topic :: Internet :: WWW/HTTP :: Browsers",
30
+ "Topic :: Software Development :: Libraries :: Python Modules",
31
+ ]
32
+ dependencies = [
33
+ "alembic>=1.18.5",
34
+ "asyncpg>=0.31.0",
35
+ "beautifulsoup4>=4.15.0",
36
+ "fsspec[gcs]>=2026.4.0",
37
+ "playwright>=1.60.0",
38
+ "sqlalchemy[asyncio]>=2.0.50",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/opensanctions/pravda"
43
+ Documentation = "https://github.com/opensanctions/pravda#readme"
44
+ Repository = "https://github.com/opensanctions/pravda"
45
+ Issues = "https://github.com/opensanctions/pravda/issues"
46
+
47
+ [tool.uv.build-backend]
48
+ module-name = "pravda"
49
+
50
+ [tool.ruff]
51
+ target-version = "py313"
52
+
53
+ [tool.ruff.lint]
54
+ select = ["E", "F", "I", "W"]
55
+
56
+ [dependency-groups]
57
+ dev = [
58
+ "pre-commit>=4.6.0",
59
+ "pytest>=9.0.3",
60
+ "pytest-asyncio>=1.4.0",
61
+ "ruff>=0.15.16",
62
+ ]
63
+
64
+ [tool.pytest.ini_options]
65
+ asyncio_mode = "auto"
66
+ asyncio_default_fixture_loop_scope = "session"
67
+ asyncio_default_test_loop_scope = "session"
@@ -0,0 +1,32 @@
1
+ """Durable evidence capture for web pages.
2
+
3
+ Construct a :class:`Pravda` with explicit :class:`PravdaConfig` settings::
4
+
5
+ from pravda import Pravda, PravdaConfig
6
+
7
+ config = PravdaConfig(
8
+ database_url=...,
9
+ browser_ws_url=...,
10
+ storage_base_path=...,
11
+ )
12
+ async with Pravda(config) as pravda:
13
+ snapshot = await pravda.snapshot(url)
14
+ history = await pravda.snapshots(url)
15
+
16
+ Applications own the Postgres database; bring it to the packaged schema head
17
+ from startup with the migration helper::
18
+
19
+ import pravda
20
+ await pravda.migrate(database_url)
21
+ """
22
+
23
+ from pravda.migrate import migrate
24
+ from pravda.pravda import Pravda, PravdaConfig
25
+ from pravda.snapshots import Snapshot
26
+
27
+ __all__ = [
28
+ "Pravda",
29
+ "PravdaConfig",
30
+ "Snapshot",
31
+ "migrate",
32
+ ]