[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "crawlee" version = "1.7.1" description = "Crawlee for Python" authors = [{ name = "Apify Technologies s.r.o.", email = "support@apify.com" }] license = { file = "LICENSE" } readme = "README.md" requires-python = ">=3.10" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Topic :: Software Development :: Libraries", ] keywords = [ "apify", "automation", "chrome", "crawlee", "crawler", "headless", "scraper", "scraping", ] dependencies = [ "async-timeout>=5.0.1", "cachetools>=5.5.0", "colorama>=0.4.0", "impit>=0.8.0", "more-itertools>=10.2.0", "protego>=0.5.0", "psutil>=6.0.0", "pydantic-settings>=2.12.0", "pydantic>=2.11.0", "pyee>=9.0.0", "tldextract>=5.1.0", "typing-extensions>=4.10.0", "yarl>=1.18.0", ] [project.optional-dependencies] all = ["crawlee[adaptive-crawler,beautifulsoup,cli,curl-impersonate,httpx,parsel,playwright,otel,sql_sqlite,sql_postgres,sql_mysql,stagehand,redis]"] adaptive-crawler = [ "jaro-winkler>=2.0.3", "playwright>=1.27.0", "scikit-learn>=1.6.0", "apify_fingerprint_datapoints>=0.0.3", "browserforge>=1.2.4" ] beautifulsoup = ["beautifulsoup4[lxml]>=4.12.0", "html5lib>=1.0"] cli = ["cookiecutter>=2.6.0", "inquirer>=3.3.0", "rich>=13.9.0", "typer>=0.12.0"] curl-impersonate = ["curl-cffi>=0.9.0"] httpx = ["httpx[brotli,http2,zstd]>=0.27.0", "apify_fingerprint_datapoints>=0.0.2", "browserforge>=1.2.3"] parsel = ["parsel>=1.10.0"] playwright = ["playwright>=1.27.0", "apify_fingerprint_datapoints>=0.0.2", "browserforge>=1.2.3"] otel = [ "opentelemetry-api>=1.34.1", "opentelemetry-distro[otlp]>=0.54", "opentelemetry-instrumentation>=0.54", "opentelemetry-instrumentation-httpx>=0.54", "opentelemetry-sdk>=1.34.1", "opentelemetry-semantic-conventions>=0.54", "wrapt>=1.17.0", ] sql_postgres = [ "sqlalchemy[asyncio]>=2.0.0,<3.0.0", "asyncpg>=0.24.0" ] stagehand = [ "stagehand>=3.19.5", "playwright>=1.27.0", "apify_fingerprint_datapoints>=0.0.2", "browserforge>=1.2.3", ] sql_sqlite = [ "sqlalchemy[asyncio]>=2.0.0,<3.0.0", "aiosqlite>=0.21.0", ] sql_mysql = [ "sqlalchemy[asyncio]>=2.0.0,<3.0.0", "aiomysql>=0.3.2", "cryptography>=46.0.5", ] redis = ["redis[hiredis] >= 7.0.0"] [project.scripts] crawlee = "crawlee._cli:cli" [project.urls] "Apify Homepage" = "https://apify.com" "Changelog" = "https://crawlee.dev/python/docs/changelog" "Discord" = "https://discord.com/invite/jyEM2PRvMU" "Documentation" = "https://crawlee.dev/python/docs/quick-start" "Homepage" = "https://crawlee.dev/python" "Issue Tracker" = "https://github.com/apify/crawlee-python/issues" "Release Notes" = "https://crawlee.dev/python/docs/upgrading" "Source Code" = "https://github.com/apify/crawlee-python" [dependency-groups] dev = [ # TODO: Remove this constraint once pydoc-markdown updates its dependencies. # Package pydoc-markdown is unmaintained and pins old docspec-python with vulnerable black. # See https://github.com/apify/apify-client-python/pull/582/ for more details. # We explicitly constrain black>=24.3.0 to override the transitive dependency. "black>=24.3.0", "anyio<5.0.0", "apify_client", # For e2e tests. "build<2.0.0", # For e2e tests. "dycw-pytest-only<3.0.0", "fakeredis[probabilistic,json,lua]<3.0.0", "poethepoet<1.0.0", "pre-commit<5.0.0", "proxy-py<3.0.0", "pydoc-markdown<5.0.0", "pytest-asyncio<2.0.0", "pytest-cov<8.0.0", "pytest-rerunfailures<17.0.0", "pytest-timeout<3.0.0", "pytest-xdist<4.0.0", "pytest<10.0.0", "ruff~=0.15.0", "setuptools", # setuptools are used by pytest, but not explicitly required "ty~=0.0.0", "types-beautifulsoup4<5.0.0", "types-cachetools<7.0.0", "types-colorama<1.0.0", "types-psutil<8.0.0", "types-python-dateutil<3.0.0", "uvicorn[standard]<1.0.0", ] [tool.hatch.build.targets.wheel] packages = ["src/crawlee"] [tool.hatch.build.targets.sdist] only-include = [ "src/crawlee", "CHANGELOG.md", "CONTRIBUTING.md", "LICENSE", "README.md", "pyproject.toml", ] [tool.ruff] line-length = 120 include = ["src/**/*.py", "tests/**/*.py", "docs/**/*.py", "website/**/*.py"] exclude = [ "website/versioned_docs/**", ] extend-exclude = ["src/crawlee/project_template"] [tool.ruff.lint] select = ["ALL"] ignore = [ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed in {filename} "ASYNC109", # Async function definition with a `timeout` parameter "BLE001", # Do not catch blind exception "C901", # `{name}` is too complex "COM812", # This rule may cause conflicts when used with the formatter "D100", # Missing docstring in public module "D104", # Missing docstring in public package "D107", # Missing docstring in `__init__` "D203", # One blank line required before class docstring "D213", # Multi-line docstring summary should start at the second line "D413", # Missing blank line after last section "EM", # flake8-errmsg "G004", # Logging statement uses f-string "ISC001", # This rule may cause conflicts when used with the formatter "FIX", # flake8-fixme "PLR0911", # Too many return statements "PLR0912", # Too many branches "PLR0913", # Too many arguments in function definition "PLR0915", # Too many statements "PYI034", # `__aenter__` methods in classes like `{name}` usually return `self` at runtime "PYI036", # The second argument in `__aexit__` should be annotated with `object` or `BaseException | None` "S102", # Use of `exec` detected "S105", # Possible hardcoded password assigned to "S106", # Possible hardcoded password assigned to argument: "{name}" "S301", # `pickle` and modules that wrap it can be unsafe when used to deserialize untrusted data, possible security issue "S303", # Use of insecure MD2, MD4, MD5, or SHA1 hash function "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes "TD002", # Missing author in TODO; try: `# TODO(): ...` or `# TODO @: ... "TRY003", # Avoid specifying long messages outside the exception class ] [tool.ruff.format] quote-style = "single" indent-style = "space" [tool.ruff.lint.per-file-ignores] "**/__init__.py" = [ "F401", # Unused imports ] "**/{tests}/*" = [ "ASYNC230", # Async functions should not open files with blocking methods like `open` "D", # Everything from the pydocstyle "INP001", # File {filename} is part of an implicit namespace package, add an __init__.py "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "S101", # Use of assert detected "SLF001", # Private member accessed: `{name}` "T20", # flake8-print "TRY301", # Abstract `raise` to an inner function ] "**/{docs,website}/**" = [ "D", # Everything from the pydocstyle "INP001", # File {filename} is part of an implicit namespace package, add an __init__.py "F841", # Local variable {variable} is assigned to but never used "N999", # Invalid module name "T201", # `print` found ] "**/docs/examples/code_examples/*crawler_with_error_snapshotter.py" = [ "PLR2004", # Magic value used in comparison. Ignored for simplicity and readability of example code. ] "**/docs/guides/code_examples/running_in_web_server/server.py" = [ "TC002", # ruff false positive. Import actually needed during runtime. ] "**/docs/guides/code_examples/creating_web_archive/*.*" = [ "ASYNC230", # Ignore for simplicity of the example. ] [tool.ruff.lint.flake8-quotes] docstring-quotes = "double" inline-quotes = "single" [tool.ruff.lint.flake8-type-checking] runtime-evaluated-base-classes = [ "pydantic.BaseModel", "pydantic_settings.BaseSettings", ] [tool.ruff.lint.flake8-builtins] builtins-ignorelist = ["id"] [tool.ruff.lint.isort] known-first-party = ["crawlee"] [tool.pytest.ini_options] addopts = "-r a --verbose" asyncio_default_fixture_loop_scope = "function" asyncio_mode = "auto" timeout = 1800 markers = [ "run_alone: marks tests that must run in isolation", ] # Ignore DeprecationWarnings coming from Uvicorn's internal imports. Uvicorn relies on deprecated # modules from `websockets`, which triggers warnings during tests. These are safe to ignore until # Uvicorn updates its internals. filterwarnings = [ "ignore:websockets.legacy is deprecated:DeprecationWarning", "ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning", ] [tool.ty.environment] python-version = "3.10" [tool.ty.src] include = ["src", "tests", "scripts", "docs", "website"] exclude = [ "src/crawlee/project_template", "docs/guides/code_examples/storage_clients/custom_storage_client_example.py", "website/versioned_docs", ] [tool.ty.rules] unused-ignore-comment = "error" [[tool.ty.overrides]] include = [ "docs/**/*.py", "website/**/*.py", ] [tool.ty.overrides.rules] unresolved-import = "ignore" [tool.coverage.report] exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:", "assert_never()"] [tool.ipdb] context = 7 [tool.uv] # Minimal defense against supply-chain attacks. exclude-newer = "24 hours" [tool.uv.exclude-newer-package] # Allow internal Apify packages to install immediately. apify-client = false apify-shared = false apify_fingerprint_datapoints = false crawlee = false # Run tasks with: uv run poe [tool.poe.tasks] clean = "rm -rf .coverage .pytest_cache .ruff_cache .ty_cache .uv-cache build coverage-unit.xml dist htmlcov website/.docusaurus website/module_shortcuts.json website/node_modules " install-sync = "uv sync --all-extras" build = "uv build --verbose" publish-to-pypi = "uv publish --verbose --token ${APIFY_PYPI_TOKEN_CRAWLEE}" type-check = "uv run ty check" check-code = ["lint", "type-check", "unit-tests"] [tool.poe.tasks.install-dev] shell = "uv sync --all-extras && uv run pre-commit install && uv run playwright install" [tool.poe.tasks.lint] shell = "uv run ruff format --check && uv run ruff check" [tool.poe.tasks.format] shell = "uv run ruff check --fix && uv run ruff format" [tool.poe.tasks.unit-tests] shell = """ uv run pytest \ --numprocesses=1 \ -m "run_alone" \ tests/unit && \ uv run pytest \ --numprocesses=${TESTS_CONCURRENCY:-auto} \ -m "not run_alone" \ tests/unit """ [tool.poe.tasks.unit-tests-cov] shell = """ uv run pytest \ --numprocesses=1 \ -m "run_alone" \ --cov=src/crawlee \ --cov-report=xml:coverage-unit.xml \ tests/unit && \ uv run pytest \ --numprocesses=${TESTS_CONCURRENCY:-auto} \ -m "not run_alone" \ --cov=src/crawlee \ --cov-report=xml:coverage-unit.xml \ --cov-append \ tests/unit """ [tool.poe.tasks.e2e-templates-tests] cmd = """ uv run pytest \ --numprocesses=${TESTS_CONCURRENCY:-auto} \ --basetemp=./.pytest-tmp \ tests/e2e/project_template """ [tool.poe.tasks.update-docs-theme] shell = "pnpm update @apify/docs-theme" cwd = "website" [tool.poe.tasks.build-docs] shell = "./build_api_reference.sh && pnpm install && uv run pnpm build" cwd = "website" [tool.poe.tasks.run-docs] shell = "./build_api_reference.sh && pnpm install && uv run pnpm start" cwd = "website"