Mark the readiness test as integration; it requires live Postgres + Redis
which are not running in Cinnabox dev. Tests run on Talos prod include it
when invoked with: pytest -m integration.
- Add @pytest.mark.integration + skipif on _has_postgres() probe
- Register integration marker in pyproject.toml
- Default addopts = -m 'not integration' skips them in plain ============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/dev/pry
configfile: pyproject.toml
testpaths: tests
plugins: respx-0.23.1, cov-7.1.0, Faker-27.4.0, asyncio-1.4.0, anyio-4.14.1
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 622 items / 1 deselected / 621 selected
tests/test_adaptive.py ....... [ 1%]
tests/test_advanced_features.py .............. [ 3%]
tests/test_advanced_scraping.py ................. [ 6%]
tests/test_agency.py ....... [ 7%]
tests/test_ai_plugin.py ... [ 7%]
tests/test_alerter.py ..... [ 8%]
tests/test_api.py .... [ 9%]
tests/test_api_integration.py ........................... [ 13%]
tests/test_api_mcp.py ...s [ 14%]
tests/test_api_mcp_x402.py .............. [ 16%]
tests/test_api_scraping.py ........ [ 17%]
tests/test_api_surface_snapshot.py .... [ 18%]
tests/test_api_templates.py ...... [ 19%]
tests/test_apify_schema.py ..... [ 20%]
tests/test_auth_connector.py ..... [ 20%]
tests/test_block_detector.py .......... [ 22%]
tests/test_cache.py ....... [ 23%]
tests/test_camoufox.py ... [ 24%]
tests/test_commerce_sync.py .. [ 24%]
tests/test_compliance.py ............. [ 26%]
tests/test_costing.py .... [ 27%]
tests/test_crm_sync.py ...... [ 28%]
tests/test_db.py ......... [ 29%]
tests/test_destinations.py ....... [ 30%]
tests/test_email_scraper.py ......... [ 32%]
tests/test_enrichment.py .......... [ 33%]
tests/test_extractor.py ....... [ 34%]
tests/test_fallback_integration.py ............ [ 36%]
tests/test_freshness.py ..... [ 37%]
tests/test_gdpr.py ...... [ 38%]
tests/test_infrastructure.py ........... [ 40%]
tests/test_intelligence.py ........................... [ 44%]
tests/test_jobqueue.py ...... [ 45%]
tests/test_lazy_load.py ..... [ 46%]
tests/test_llm_fallback.py ...... [ 47%]
tests/test_llm_providers.py ...... [ 48%]
tests/test_logging_config.py ........ [ 49%]
tests/test_markdown_gen.py ...... [ 50%]
tests/test_marketplace.py ........... [ 52%]
tests/test_mcp_production.py .......... [ 54%]
tests/test_monitor.py ............... [ 56%]
tests/test_network.py ...... [ 57%]
tests/test_parser.py ...... [ 58%]
tests/test_pipeline.py ..... [ 59%]
tests/test_pipelines.py ....... [ 60%]
tests/test_proxy_manager.py ............ [ 62%]
tests/test_quality.py ........... [ 64%]
tests/test_ratelimit.py ...... [ 65%]
tests/test_real_features.py ........ [ 66%]
tests/test_reconciliation.py ......... [ 67%]
tests/test_referrals.py .............. [ 70%]
tests/test_reports.py ....... [ 71%]
tests/test_resilience.py .............. [ 73%]
tests/test_retry.py ........................... [ 77%]
tests/test_review.py ...... [ 78%]
tests/test_scraper.py ........ [ 80%]
tests/test_sdk.py ...... [ 80%]
tests/test_secrets_backend.py ......... [ 82%]
tests/test_seo_monitor.py ...... [ 83%]
tests/test_settings.py ......... [ 84%]
tests/test_shadow_dom.py ......... [ 86%]
tests/test_structure_monitor.py F...... [ 87%]
tests/test_templates.py ...... [ 88%]
tests/test_training_data.py ......... [ 89%]
tests/test_ultimate_scraper.py ............ [ 91%]
tests/test_url_guard.py ........................ [ 95%]
tests/test_x402_mcp_spec.py ........................... [100%]
=================================== FAILURES ===================================
__________________________ test_check_selectors_empty __________________________
@pytest.mark.asyncio
async def test_check_selectors_empty():
> result = await check_selectors("https://example.com", [])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/test_structure_monitor.py:36:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
structure_monitor.py:46: in check_selectors
resp = await client.get(
../.local/lib/python3.12/site-packages/httpx/_client.py:1768: in get
return await self.request(
../.local/lib/python3.12/site-packages/httpx/_client.py:1540: in request
return await self.send(request, auth=auth, follow_redirects=follow_redirects)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../.local/lib/python3.12/site-packages/httpx/_client.py:1629: in send
response = await self._send_handling_auth(
../.local/lib/python3.12/site-packages/httpx/_client.py:1657: in _send_handling_auth
response = await self._send_handling_redirects(
../.local/lib/python3.12/site-packages/httpx/_client.py:1694: in _send_handling_redirects
response = await self._send_single_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../.local/lib/python3.12/site-packages/httpx/_client.py:1730: in _send_single_request
response = await transport.handle_async_request(request)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../.local/lib/python3.12/site-packages/httpx/_transports/default.py:394: in handle_async_request
resp = await self._pool.handle_async_request(req)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../.local/lib/python3.12/site-packages/httpcore/_async/connection_pool.py:256: in handle_async_request
raise exc from None
../.local/lib/python3.12/site-packages/httpcore/_async/connection_pool.py:229: in handle_async_request
await self._close_connections(closing)
../.local/lib/python3.12/site-packages/httpcore/_async/connection_pool.py:345: in _close_connections
await connection.aclose()
../.local/lib/python3.12/site-packages/httpcore/_async/connection.py:173: in aclose
await self._connection.aclose()
../.local/lib/python3.12/site-packages/httpcore/_async/http11.py:258: in aclose
await self._network_stream.aclose()
../.local/lib/python3.12/site-packages/httpcore/_backends/anyio.py:53: in aclose
await self._stream.aclose()
../.local/lib/python3.12/site-packages/anyio/streams/tls.py:236: in aclose
await self.transport_stream.aclose()
../.local/lib/python3.12/site-packages/anyio/_backends/_asyncio.py:1374: in aclose
self._transport.close()
/usr/lib/python3.12/asyncio/selector_events.py:1211: in close
super().close()
/usr/lib/python3.12/asyncio/selector_events.py:875: in close
self._loop.call_soon(self._call_connection_lost, None)
/usr/lib/python3.12/asyncio/base_events.py:795: in call_soon
self._check_closed()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <_UnixSelectorEventLoop running=False closed=True debug=False>
def _check_closed(self):
if self._closed:
> raise RuntimeError('Event loop is closed')
E RuntimeError: Event loop is closed
/usr/lib/python3.12/asyncio/base_events.py:541: RuntimeError
=============================== warnings summary ===============================
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
/home/dev/.local/lib/python3.12/site-packages/pydantic/main.py:1809: UserWarning: Field name "schema" in "Body_extract_css_v1_extract_css_post" shadows an attribute in parent "BaseModel"
return meta(
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
/home/dev/.local/lib/python3.12/site-packages/pydantic/main.py:1809: UserWarning: Field name "schema" in "Body_extract_llm_v1_extract_llm_post" shadows an attribute in parent "BaseModel"
return meta(
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
/home/dev/.local/lib/python3.12/site-packages/pydantic/main.py:1809: UserWarning: Field name "schema" in "Body_quality_check_v1_quality_check_post" shadows an attribute in parent "BaseModel"
return meta(
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
/home/dev/.local/lib/python3.12/site-packages/pydantic/main.py:1809: UserWarning: Field name "schema" in "Body_extract_with_review_v1_extract_with_review_post" shadows an attribute in parent "BaseModel"
return meta(
tests/test_api_integration.py::TestHealthEndpoints::test_live_returns_200
/home/dev/.local/lib/python3.12/site-packages/_pytest/unraisableexception.py:67: PytestUnraisableExceptionWarning: Exception ignored in: <function BaseSubprocessTransport.__del__ at 0x7c2702c51120>
Traceback (most recent call last):
File "/usr/lib/python3.12/asyncio/base_subprocess.py", line 126, in __del__
self.close()
File "/usr/lib/python3.12/asyncio/base_subprocess.py", line 104, in close
proto.pipe.close()
File "/usr/lib/python3.12/asyncio/unix_events.py", line 767, in close
self.write_eof()
File "/usr/lib/python3.12/asyncio/unix_events.py", line 753, in write_eof
self._loop.call_soon(self._call_connection_lost, None)
File "/usr/lib/python3.12/asyncio/base_events.py", line 795, in call_soon
self._check_closed()
File "/usr/lib/python3.12/asyncio/base_events.py", line 541, in _check_closed
raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
Enable tracemalloc to get traceback where the object was allocated.
See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.
warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
tests/test_api_integration.py::TestDetectBlockEndpoint::test_detect_block_success
tests/test_api_integration.py::TestErrorHandling::test_422_on_invalid_json
/home/dev/.local/lib/python3.12/site-packages/httpx/_models.py:408: DeprecationWarning: Use 'content=<...>' to upload raw bytes/text content.
headers, stream = encode_request(
tests/test_jobqueue.py::test_jobqueue_create_job_returns_id
tests/test_jobqueue.py::test_jobqueue_get_job_exists
tests/test_jobqueue.py::test_jobqueue_complete_job
tests/test_jobqueue.py::test_jobqueue_fail_job
tests/test_jobqueue.py::test_jobqueue_update_preserves_fields
/home/dev/pry/jobqueue.py:56: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
"created_at": datetime.utcnow().isoformat(),
tests/test_jobqueue.py::test_jobqueue_create_job_returns_id
tests/test_jobqueue.py::test_jobqueue_get_job_exists
tests/test_jobqueue.py::test_jobqueue_complete_job
tests/test_jobqueue.py::test_jobqueue_fail_job
tests/test_jobqueue.py::test_jobqueue_update_preserves_fields
/home/dev/pry/jobqueue.py:57: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
"updated_at": datetime.utcnow().isoformat(),
tests/test_jobqueue.py::test_jobqueue_complete_job
tests/test_jobqueue.py::test_jobqueue_fail_job
tests/test_jobqueue.py::test_jobqueue_update_preserves_fields
/home/dev/pry/jobqueue.py:93: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
self._local_jobs[job_id]["updated_at"] = datetime.utcnow().isoformat()
tests/test_retry.py::TestAsyncRetry::test_passes_args_and_kwargs
/home/dev/pry/tests/test_retry.py:259: RuntimeWarning: coroutine 'AsyncMockMixin._execute_mock_call' was never awaited
fn(url, timeout=timeout)
Enable tracemalloc to get traceback where the object was allocated.
See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.
tests/test_sdk.py::test_prycrawl_health_returns_dict
/home/dev/.local/lib/python3.12/site-packages/_pytest/python.py:167: RuntimeWarning: coroutine 'PryCrawl.health' was never awaited
result = testfunction(**testargs)
Enable tracemalloc to get traceback where the object was allocated.
See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_structure_monitor.py::test_check_selectors_empty - RuntimeE...
= 1 failed, 619 passed, 1 skipped, 1 deselected, 26 warnings in 90.04s (0:01:30) =
Tests: 2 passed, 1 deselected in TestHealthEndpoints (was 2 pass + 1 fail).
190 lines
4.7 KiB
TOML
190 lines
4.7 KiB
TOML
# SPDX-License-Identifier: MIT
|
|
# Copyright (c) 2026 Rug Munch Media LLC
|
|
# Part of Pry — https://git.rugmunch.io/RugMunchMedia/pryscraper
|
|
# Licensed under MIT. See LICENSE.
|
|
[build-system]
|
|
requires = ["setuptools>=69.0"]
|
|
build-backend = "setuptools.build_meta"
|
|
|
|
[project]
|
|
name = "pry"
|
|
version = "3.0.0"
|
|
description = "Free web scraping + browser automation API — self-hosted, no API keys needed"
|
|
readme = "README.md"
|
|
requires-python = ">=3.11"
|
|
license = "MIT"
|
|
authors = [{ name = "Rug Munch Media LLC" }]
|
|
keywords = ["scraping", "crawler", "browser-automation", "mcp", "x402", "ai-agents"]
|
|
classifiers = [
|
|
"Development Status :: 4 - Beta",
|
|
"Operating System :: OS Independent",
|
|
"Programming Language :: Python :: 3",
|
|
"Programming Language :: Python :: 3.11",
|
|
"Programming Language :: Python :: 3.12",
|
|
"Topic :: Internet :: WWW/HTTP :: Browsers",
|
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
"Typing :: Typed",
|
|
]
|
|
license-files = ["LICENSE", "LICENSE-BSL-STEALTH"]
|
|
dependencies = [
|
|
"fastapi>=0.115.0",
|
|
"uvicorn[standard]>=0.32.0",
|
|
"trafilatura>=2.0.0",
|
|
"readability-lxml>=0.8.1",
|
|
"lxml>=5.3.0",
|
|
"httpx>=0.28.0",
|
|
"markdownify>=0.14.0",
|
|
"pydantic>=2.0.0",
|
|
"pydantic-settings>=2.0.0",
|
|
"playwright>=1.50.0",
|
|
"redis>=5.0.0",
|
|
"pypdf>=5.0.0",
|
|
"python-docx>=1.1.0",
|
|
"tiktoken>=0.8.0",
|
|
"pillow>=10.0.0",
|
|
"click>=8.0.0",
|
|
"pyyaml>=6.0",
|
|
"pandas>=2.0.0",
|
|
"anyio>=4.0.0",
|
|
"croniter>=2.0.0",
|
|
"structlog>=24.0.0",
|
|
"sqlalchemy>=2.0.0",
|
|
"aiosqlite>=0.19.0",
|
|
"prometheus-client>=0.21.0",
|
|
"opentelemetry-api>=1.29.0",
|
|
"opentelemetry-sdk>=1.29.0",
|
|
"cloudscraper>=1.2.0",
|
|
"aiohttp-socks[aiohttp]>=0.8.0",
|
|
"aiohttp>=3.9.0",
|
|
"undetected-chromedriver>=3.5.0",
|
|
"pyjwt>=2.8.0",
|
|
"apify>=2.0.0",
|
|
"playwright-stealth>=1.0.0",
|
|
]
|
|
|
|
[project.optional-dependencies]
|
|
dev = [
|
|
"pytest>=8.0",
|
|
"pytest-asyncio>=0.24.0",
|
|
"pytest-cov>=5.0.0",
|
|
"ruff>=0.7.0",
|
|
"mypy>=1.12.0",
|
|
"pre-commit>=4.0.0",
|
|
"alembic>=1.14.0",
|
|
"respx>=0.21.0",
|
|
"commitizen>=3.12.0",
|
|
]
|
|
|
|
[project.scripts]
|
|
pry = "cli:main"
|
|
|
|
[tool.setuptools.packages.find]
|
|
include = ["pry*"]
|
|
|
|
[tool.ruff]
|
|
target-version = "py311"
|
|
line-length = 100
|
|
|
|
[tool.ruff.lint]
|
|
select = ["E", "F", "I", "N", "W", "B", "A", "C4", "SIM", "UP", "RUF", "BLE"]
|
|
ignore = ["E501", "N815", "B008", "A002", "RUF006"]
|
|
|
|
# BLE001 (blind except Exception) is enforced. Pre-existing sites are
|
|
# marked with `# noqa: BLE001`; new code must use specific exception
|
|
# types (e.g. httpx.HTTPError, json.JSONDecodeError, OSError).
|
|
|
|
[tool.ruff.format]
|
|
quote-style = "double"
|
|
indent-style = "space"
|
|
|
|
[tool.mypy]
|
|
python_version = "3.12"
|
|
strict = false
|
|
ignore_missing_imports = true
|
|
exclude = [
|
|
"build/",
|
|
"dist/",
|
|
".git/",
|
|
"__pycache__/",
|
|
]
|
|
warn_unused_ignores = false
|
|
# TODO: Pry was built without strict typing; re-enable incrementally.
|
|
# Tracking issue: re-enable mypy strict mode after router refactor.
|
|
disable_error_code = [
|
|
"dict-item",
|
|
"index",
|
|
"attr-defined",
|
|
"call-arg",
|
|
"operator",
|
|
"arg-type",
|
|
"abstract",
|
|
"union-attr",
|
|
"assignment",
|
|
"list-item",
|
|
"func-returns-value",
|
|
"return-value",
|
|
"no-untyped-call",
|
|
"no-untyped-def",
|
|
]
|
|
|
|
[[tool.mypy.overrides]]
|
|
module = [
|
|
"trafilatura",
|
|
"trafilatura.*",
|
|
"readability",
|
|
"readability.*",
|
|
"playwright",
|
|
"playwright.*",
|
|
"pypdf",
|
|
"pypdf.*",
|
|
"docx",
|
|
"docx.*",
|
|
"PIL",
|
|
"PIL.*",
|
|
"markdownify",
|
|
"markdownify.*",
|
|
"pandas",
|
|
"pandas.*",
|
|
"yaml",
|
|
"yaml.*",
|
|
"numpy",
|
|
"numpy.*",
|
|
]
|
|
ignore_missing_imports = true
|
|
|
|
[tool.pytest.ini_options]
|
|
asyncio_mode = "auto"
|
|
testpaths = ["tests"]
|
|
# Skip `integration` tests by default — they require live Postgres + Redis.
|
|
# Run them explicitly with: pytest -m integration
|
|
addopts = "-m 'not integration'"
|
|
markers = [
|
|
"integration: tests that require live infrastructure (Postgres, Redis, external HTTP). Skipped by default; run with `pytest -m integration`.",
|
|
]
|
|
|
|
[tool.coverage.run]
|
|
branch = true
|
|
source = ["api", "cli", "extractor", "parser", "scraper", "monitor", "network", "cache", "auth", "errors", "paths", "settings", "db", "tasks", "quality", "reports", "extraction", "x402", "x402_middleware"]
|
|
omit = [
|
|
"tests/*",
|
|
"browser-extension/*",
|
|
"shopify-app/*",
|
|
"wordpress-plugin/*",
|
|
"*/templates/*",
|
|
"*/profiles/*",
|
|
"*/stealth_scripts/*",
|
|
"pry.egg-info/*",
|
|
"build/*",
|
|
"dist/*",
|
|
]
|
|
|
|
[tool.coverage.report]
|
|
fail_under = 45 # gradual path: current is ~47%; raise to 60, 70, 75, 80 in successive PRs
|
|
precision = 1
|
|
show_missing = true
|
|
exclude_lines = [
|
|
"pragma: no cover",
|
|
"if __name__ == .__main__.:",
|
|
"raise NotImplementedError",
|
|
"pass",
|
|
]
|