graph-crawler 5.3.3__tar.gz → 5.3.5__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.
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/CHANGELOG.md +113 -0
- {graph_crawler-5.3.3/graph_crawler.egg-info → graph_crawler-5.3.5}/PKG-INFO +56 -65
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/README.md +55 -64
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/_core.py +2 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/async_.py +2 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/client/client.py +3 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/sync.py +4 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/dto/mappers/node_mapper.py +1 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/crawl_coordinator.py +22 -3
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/filters/domain_filter.py +31 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/node_scanner.py +5 -5
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/sitemap_parser.py +84 -12
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/sitemap_processor.py +3 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/sitemap_spider.py +1 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/spider.py +75 -36
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/entities/graph.py +129 -7
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/entities/node.py +163 -11
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/entities/sitemap_node.py +3 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/__init__.py +2 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/control_channel.py +27 -17
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/configs.py +3 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/sqlite_eviction_storage.py +1 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/escalation.py +128 -19
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/isolated_executor.py +7 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5/graph_crawler.egg-info}/PKG-INFO +56 -65
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/pyproject.toml +1 -1
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/LICENSE +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/MANIFEST.in +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/__version__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/ai/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/_distributed.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/_shared.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/_sitemap_distributed.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/cli.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/client/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/history_manager.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/project_init.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/server/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/server/crawl_monitor.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/server/dashboard.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/server/rest_api.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/server/webhooks.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/server/websocket_manager.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/api/stats_collector.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/bootstrap.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/context/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/context/dependency_registry.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/context/graph_context.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/context/merge_context.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/dto/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/dto/edge_dto.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/dto/graph_dto.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/dto/mappers/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/dto/mappers/edge_mapper.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/dto/mappers/graph_mapper.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/dto/mappers/node_data_codec.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/dto/node_dto.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/dto/utils.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/application_container.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/driver_factory.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/base_exporter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/csv_exporter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/edge_exporter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/excel_exporter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/jsonl_exporter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/node_exporter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/parquet_exporter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/sql_exporter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/finalize.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/low_memory_graph_saver.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/resource_budget.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/scheduling/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/scheduling/rule_based_scheduler.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/services/storage_factory.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/adaptive_throttler.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/async_bridge.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/base_spider.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/celery_batch_spider.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/celery_spider.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/checkpoint.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/dead_letter_queue.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/domain_rate_limiter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/filters/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/filters/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/filters/domain_patterns.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/filters/path_filter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/incremental_strategy.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/link_processor.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/multiprocess_spider.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/parsers/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/parsers/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/parsers/html_parser.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/progress_tracker.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/scheduler.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/serialization_mixin.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/spider_lifecycle.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/worker_pool.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/graph_export.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/graph_merge/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/graph_merge/graph_operations.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/graph_merge/merge_strategies.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/data/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/data/backends/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/data/backends/memory.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/data/backends/sqlite.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/data/interfaces.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/context/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/context/crawl_context.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/entities/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/entities/edge.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/entities/edge_analysis.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/entities/graph_operations.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/entities/graph_statistics.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/entities/merge_strategies.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/entities/redirect_mode.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/entities/registries.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/entities/strategies.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/crawl.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/edge.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/event_bus.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/events.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/fetch.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/idempotent_subscriber.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/legacy_type_map.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/llm.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/node.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/plugin.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/scheduler.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/events/storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/adapter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/crawl_context.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/distributed_spider.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/driver.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/event_bus.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/exporters.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/filter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/merge_context.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/node_interfaces.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/parser.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/plugin_manager.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/processor.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/scanner.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/scheduler.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/scheduling/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/scheduling/frontier.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/scheduling/scheduler.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/spider.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/stop_condition.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/storage/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/storage/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/storage/eviction.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/storage/queue.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/adjacency_maintenance.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/cycle_detection.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/degree_ranking.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/depth_context.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/display_title.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/duplicate_detection.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/duplicate_selector.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/duplicate_stats.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/edge_filter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/edge_statistics.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/eviction_policy.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/graph_query_cascade.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/graph_statistics.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/graph_traversal.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/language_versions.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/lsh_index.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/minimap_builder.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/node_content_resolver.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/node_view_builder.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/pagination_guards.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/predicate_ops.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/schema_filter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/search_haystack.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/simhash_clustering.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/simhash_fallback.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/similarity_ranking.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/text_search.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/url_clustering.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/url_deduplication.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/url_normalizer.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/url_rule_matcher.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/services/url_tree_builder.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/domain_patterns.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/fetch_options.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/html_document.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/lifecycle.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/models.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/page_actions.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/redirect_hop.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/scheduling/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/scheduling/crawl_request.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/scheduling/retry_policy.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/settings.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/url_normalization.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/views.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/cache_middleware.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/chain.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/error_recovery_middleware.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/logging_middleware.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/proxy_health.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/proxy_middleware.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/proxy_models.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/proxy_selection.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/rate_limit_middleware.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/request_response_middleware.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/retry_middleware.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/robots_cache.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/robots_middleware.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/robots_validator.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/user_agent_middleware.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/builtin/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/builtin/stats_export_plugin.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/crawl_engine/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/crawl_engine/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/crawl_engine/priority_provider.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/crawl_engine/smart_crawl.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/anti_bot_detection.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/anti_bot_playwright.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/anti_bot_scripts.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/anti_bot_stealth.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/captcha/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/captcha/detector.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/captcha/models.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/captcha/plugin.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/captcha/services.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/content_extractors/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/content_extractors/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/content_extractors/goose3_extractor.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/content_extractors/newspaper_extractor.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/content_extractors/plugin.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/content_extractors/readability_extractor.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/defaults.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/extractors/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/extractors/email_extractor.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/extractors/phone_extractor.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/extractors/price_extractor.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/links.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/markdown_plugin.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/metadata.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/smart_page_finder.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/constants.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/exceptions.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/extractor.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/options.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/jsonld.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/microdata.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/opengraph.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/rdfa.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/twitter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/plugin.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/result.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/text.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/url_normalizer.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/registry.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/adapters/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/adapters/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/adapters/selectolax_adapter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/celery_app.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/celery_batch.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/celery_job_task.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/celery_unified.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/config.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/easy_crawler.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/worker_api.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/auto_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/eviction.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/graph_repository.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/json/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/json_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/lmdb_eviction_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/memory/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/memory_dedup_store.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/memory_frontier.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/memory_politeness_store.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/memory_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/mongodb_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/naming_strategy.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/postgresql_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/protocols/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/redis_dedup_store.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/redis_politeness_store.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/repository.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/sqlite/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/sqlite_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/file_job_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/file_queue_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/memory_job_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/memory_queue_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/postgresql_job_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/postgresql_queue_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/unified_storage.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/config.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/context.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/driver.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/driver_curl_cffi.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/autothrottle.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/autothrottle_adaptive.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/headers.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/http_cache.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/rate_limiter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/retry.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/stages.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/base_plugin.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/compression/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/compression/strategy.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/connection_pool.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/context.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/core/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/core/base_async.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/core/body_decoder.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/core/mixins.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/core/redirect_utils.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/factory.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/adaptive_semaphore.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/batch_scheduler.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/browser_factory.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/browser_profile.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/config.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/context.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/driver.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/engine.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/fetch_metrics.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/net/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/net/geoip.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/net/proxy_pool.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/net/rate_limiter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/page_actions.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/page_fetch.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/_network_record.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/captcha_detector.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/captcha_solver.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/cookie_consent.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/form_filler.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/human_bezier.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/network_log.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/ram_adaptive.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/screenshot.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/user_profile.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/pool_lifecycle.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/pooled_driver.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/stages.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/stealth_hints.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/plugin_manager.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/protocols.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/undetected/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/undetected/async_driver.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/native/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/native/bloom_filter.pyx +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/native/html_parser.pyx +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/native/setup.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/native/url_utils.pyx +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/decorators/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/decorators/cache.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/decorators/log.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/decorators/retry.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/decorators/timing.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/error_tracing.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/extensions/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/extensions/memory_guard.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/extensions/state_manager.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/base_metrics_listener.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/crawl_listener.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/dlq_listener.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/error_listener.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/logging_listener.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/metrics_listener.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/node_listener.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/plugin_listener.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/storage_listener.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/url_listener.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/metrics/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/metrics/memory_profiler.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/metrics/metrics_collector.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/metrics/prometheus_metrics.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/metrics_core.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/structured_logging.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/observability/tracing.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/optimizations/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/optimizations/generators.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/optimizations/memory_slots.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/optimizations/simhash_numba.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/py.typed +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/constants.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/dto/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/dto/edge_dto.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/dto/graph_dto.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/dto/node_dto.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/error_handling/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/error_handling/error_handler.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/exceptions.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/platform/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/platform/adaptive_resolver.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/platform/config.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/platform/detector.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/platform/diagnostics.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/platform/metrics.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/security/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/security/url_sanitizer.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/security/url_validator.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/async_compat.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/bloom_filter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/captcha/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/captcha/base.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/captcha/manager.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/celery_config.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/celery_helpers.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/cgroup_limits.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/content_decoder.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/distributed_rate_limiter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/distributed_rate_limiter_backends.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/dns_cache.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/enterprise_ua_generator.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/event_publisher_mixin.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/fast_json.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/fingerprint.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/fingerprint_data.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/fingerprint_generators.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/fingerprint_profile.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/fingerprint_stealth.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/html_utils.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/id_generator.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/memory_optimizer.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/rate_limiter.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/regex_validator.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/shadow_dom.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/text_utils.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/url_patterns.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/url_utils.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/user_agent_rotator.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/validation_helpers.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/visualization.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/shared/utils/visualization_core.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/tools/__init__.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler/tools/db_cleanup.py +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler.egg-info/SOURCES.txt +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler.egg-info/dependency_links.txt +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler.egg-info/entry_points.txt +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler.egg-info/requires.txt +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/graph_crawler.egg-info/top_level.txt +0 -0
- {graph_crawler-5.3.3 → graph_crawler-5.3.5}/setup.cfg +0 -0
|
@@ -1,5 +1,118 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 5.3.5 — 2026-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Dirty `Sitemap:` value in robots.txt no longer loses the sitemap (P19).** `SitemapParser._parse_robots_txt`
|
|
10
|
+
took the whole directive value verbatim (`line[8:].strip()`), so `Sitemap: sitemap URL https://site.com/sitemap_index.xml`
|
|
11
|
+
was glued into a broken address and the sitemap was lost. It now extracts the first `https?://\S+` from the value;
|
|
12
|
+
`urljoin` is applied only when the value is a relative path starting with `/`. New `SitemapParser._clean_sitemap_value`.
|
|
13
|
+
- **A 404/410 is never a bot block (P19 side-finding).** `escalation._classify` returns `BotBlockStatus.NONE` for
|
|
14
|
+
status 404/410 right after the authoritative challenge headers. A dead sitemap candidate (or any not-found page) is
|
|
15
|
+
no longer classified as `captcha`/`challenge` and dragged through every tier (http→tls→browser). A real challenge
|
|
16
|
+
uses 403/503, not 404, so the authoritative header check still wins. Invariant: 404 is a definitive "not found"
|
|
17
|
+
from the server, not a page served instead of content. (After N3, verify & lock with a test — see Parser side.)
|
|
18
|
+
- **A redirect inside the same ATS no longer blocks `domain_filter` (P21).** `CrawlCoordinator._handle_redirect_after_scan`
|
|
19
|
+
always allows a redirect to the **same host** (short ATS forms `apply.workable.com/j/X` → `apply.workable.com/<slug>/j/X`).
|
|
20
|
+
For a different host it still asks `domain_filter.is_allowed`, and when that refuses it consults a new host-alias
|
|
21
|
+
extension point `DomainFilter.allow_redirect(source, target)` (driven by a settable `redirect_host_alias` hook) — so
|
|
22
|
+
the Parser can pass its `DOMAIN_POLICIES` and allow `boards.greenhouse.io` → `job-boards.greenhouse.io` without
|
|
23
|
+
opening the whole internet.
|
|
24
|
+
- **Sitemap under a Cloudflare 52x origin error stops without retries (1.6).** On the first `520`–`527` response to
|
|
25
|
+
robots.txt / sitemap, `SitemapParser` stops the sitemap phase and returns `result["origin_down"] = True` (logged as
|
|
26
|
+
`ORIGIN_DOWN`) instead of burning 100–180 s on retries/escalation against a down origin. New
|
|
27
|
+
`SitemapParser._is_origin_down`; `origin_down` is propagated through robots / default / nested sitemap paths.
|
|
28
|
+
- **Nodes evicted by low-memory eviction are no longer lost in a browser full rescan (P23).**
|
|
29
|
+
`Graph.iter_nodes_async(include_evicted=True)` now also yields nodes that were evicted to disk (read-only, without
|
|
30
|
+
pulling them back into RAM), so a merge that iterates the source graph sees them. `Node.pinned` + `Graph.pin_node()`
|
|
31
|
+
/ `Graph.pin_seed_nodes()` keep seed (`depth == 0`) vacancies in RAM — they were the oldest and got evicted first.
|
|
32
|
+
`Graph.eviction_stats` exposes the graph's own eviction counters (a backend-less graph evicts to LMDB/SQLite, which
|
|
33
|
+
`backend.get_stats()` reported as 0).
|
|
34
|
+
- **A 403 storm without vendor markers is treated as a block (P24).** `EscalatingDriver` tracks a per-host window of
|
|
35
|
+
recent HTTP statuses; if a host that recently returned 2xx then returns 403 with no anti-bot markers and either ≥3
|
|
36
|
+
consecutive 403 or >30 % 403 in the window, the 403 is reclassified as `SOFT_BLOCK` (`escalation_reason=http_403_storm`)
|
|
37
|
+
and the tier escalates. A single 403 on a 2xx background is still treated as auth/method (no escalation). New
|
|
38
|
+
`_HostStatusWindow`.
|
|
39
|
+
- **The HTML enrichment pool is priority-aware (2.1 / §15.4).** `run_in_html_pool(fn, *args, priority=...)` admits the
|
|
40
|
+
highest-priority waiter first when the pool is saturated (job-URLs and career subdomains enrich before catalog/blog
|
|
41
|
+
pages); with a free slot and empty queue it behaves exactly like the old FIFO semaphore. `Node` passes its URL-rule
|
|
42
|
+
priority. New `_PrioritySemaphore`.
|
|
43
|
+
- **Cloudflare 520–527 is never a bot block.** `escalation._classify` returns `NONE` for 520–527 (as for 404/410):
|
|
44
|
+
the CF error page embeds `challenge-platform`, so a down origin was classified `challenge` and escalated
|
|
45
|
+
http→tls→browser (~20 s per tier) before `SitemapParser` could stop on `ORIGIN_DOWN`.
|
|
46
|
+
- **robots.txt after a domain move keeps the new host's sitemap.** The same-origin gate and relative `Sitemap:`
|
|
47
|
+
paths now use the host that actually served robots.txt (`final_url`): `kilo.health` → `kilo.co/robots.txt` with
|
|
48
|
+
`Sitemap: … https://kilo.co/sitemap_index.xml` was dropped as cross-origin even after P19 cleaning.
|
|
49
|
+
- **Cancelling a call waiting in the HTML-pool queue resumes the enrich budget.** With the priority semaphore the
|
|
50
|
+
acquire moved out of the `try`, so a cancellation in the queue left the node deadline suspended.
|
|
51
|
+
- **`Node.pinned` does not leak into `user_data['_custom_fields']`** (`NodeMapper`, SQLite eviction storage base
|
|
52
|
+
fields) — it broke DTO/user-data invariants (`test_reserved_user_data_keys`, `test_node_mapper_content_depth`).
|
|
53
|
+
- Tests: `tests/test_v535_fixes.py` (28, offline: P19, 404/52x, 1.6, P21 incl. end-to-end `async_crawl`, P23, P24, 2.1).
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- `crawl` / `async_crawl` / `GraphCrawlerClient.crawl` / `CrawlerConfig`: `redirect_host_alias=` —
|
|
58
|
+
`Callable[[source_url, target_url], bool]` installed on the spider's `DomainFilter.redirect_host_alias`
|
|
59
|
+
(the extension point was otherwise unreachable through the public API).
|
|
60
|
+
- `SitemapParser._clean_sitemap_value`, `SitemapParser._is_origin_down`; `result["origin_down"]` key.
|
|
61
|
+
- `DomainFilter.allow_redirect(source_url, target_url)` and settable `DomainFilter.redirect_host_alias` hook.
|
|
62
|
+
- `Node.pinned`; `Graph.pin_node`, `Graph.pin_seed_nodes`, `Graph.eviction_stats`; `iter_nodes_async(include_evicted=)`.
|
|
63
|
+
- `run_in_html_pool(..., priority=)`.
|
|
64
|
+
|
|
65
|
+
### Fixed (carried from Unreleased)
|
|
66
|
+
|
|
67
|
+
- **Sitemap `<lastmod>` is no longer dropped.** `SitemapParser` reads `<lastmod>` of every `<url>` (with or without
|
|
68
|
+
namespace) into `result["lastmod"]` (`{url: value}`, merged across robots/default/nested sitemaps), and
|
|
69
|
+
`SitemapProcessor.create_url_nodes(lastmod=...)` stores it on the new `SitemapNode.lastmod` field (W3C datetime as
|
|
70
|
+
in the file). Consumers can now tell a 2021 post from a fresh page; `_parse_urlset` keeps its contract.
|
|
71
|
+
|
|
72
|
+
- **An embedded captcha widget on a page with content is not a bot block.** `classify_block` follows the same
|
|
73
|
+
principle as the browser `CaptchaDetectorPlugin`: a block is a page served *instead of* content. After the
|
|
74
|
+
authoritative challenge headers (`cf-mitigated`, `x-amzn-waf-action`, `x-vercel-mitigated`), a non-blocking
|
|
75
|
+
response whose `<body>` has ≥1000 visible characters or ≥15 links is `NONE`, whatever widget or protection
|
|
76
|
+
script it embeds. Previously visible text was measured over the first 64 KB of the document (in WordPress that is
|
|
77
|
+
almost all `<head>`), and `cf-turnstile` / `challenge-platform` (Cloudflare loads its JSD script on regular pages)
|
|
78
|
+
were unconditional markers — so every page with reCAPTCHA/hCaptcha/Turnstile in a contact or apply form was
|
|
79
|
+
`captcha`, escalated HTTP→TLS and pinned the whole host to the TLS tier. `cf-turnstile`, `captcha-container`,
|
|
80
|
+
`challenge-platform`, `_incapsula_resource`, `awswafintegration` now count only on pages without content or with a
|
|
81
|
+
blocking status; markers and text are read from `<body>` up to 1 MB.
|
|
82
|
+
|
|
83
|
+
## 5.3.4 — 2026-10
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
|
|
87
|
+
- **The per-node enrichment limit measures active work, not the HTML-pool queue.** `NodeScanner` wraps
|
|
88
|
+
`enrich_node` in `enrich_budget(seconds)` (`graph_crawler.domain.entities.node`): while a call waits for a thread
|
|
89
|
+
of the HTML executor in `run_in_html_pool` (parse, `_update_from_context`, hashes), the deadline is suspended and
|
|
90
|
+
resumed with the same remainder the moment the work starts. With 5.3.3 the hook and the hashes moved into the
|
|
91
|
+
shared pool, so under load a node's wall-clock depended on how many neighbours stood in front of it: a burst of
|
|
92
|
+
large pages produced a cascade of `Per-node timeout … marking as ERROR` for pages whose own processing took a
|
|
93
|
+
fraction of the limit (and every stage re-queued behind the others). A page whose **own** processing exceeds
|
|
94
|
+
`node_enrich_timeout` still times out and still returns its discovered links. The crawl-level timeout remains the
|
|
95
|
+
upper bound for a node that waits in the queue.
|
|
96
|
+
- **`IsolatedExecutor.run` admits at most `max_workers` tasks at a time** (`threading.BoundedSemaphore`): the task
|
|
97
|
+
`timeout` now measures execution in the child process, not the wait behind other tasks. Previously a burst of
|
|
98
|
+
submissions could expire the timeout of a queued task, which killed the whole pool and failed every task in flight.
|
|
99
|
+
|
|
100
|
+
- **Control commands are no longer dropped silently.** `GraphSpider.process_control_messages` executes only
|
|
101
|
+
`SPIDER_COMMANDS` (`STOP`, `PAUSE`, `RESUME`, `REPRIORITIZE`, `FORCE_VISIT`); any other `CrawlCommand` logs
|
|
102
|
+
`WARNING Unsupported control command '<value>' from <sender> is ignored (supported: ...)` once per command type
|
|
103
|
+
per crawl (repeats at `DEBUG`) and the crawl goes on. `REPRIORITIZE` without `url`/`priority` and `FORCE_VISIT`
|
|
104
|
+
without `url` log a `WARNING` with the received `data` instead of doing nothing.
|
|
105
|
+
|
|
106
|
+
### Added
|
|
107
|
+
|
|
108
|
+
- `EnrichBudget` (`pause()` / `resume()`) and `enrich_budget()` in `graph_crawler.domain.entities.node`.
|
|
109
|
+
- `SPIDER_COMMANDS` (`graph_crawler.domain.interfaces`): the commands `GraphSpider` executes.
|
|
110
|
+
|
|
111
|
+
### Deprecated
|
|
112
|
+
|
|
113
|
+
- `CrawlCommand.SKIP_URL`, `CHANGE_STRATEGY`, `SET_DEPTH_LIMIT`, `REQUEST_STATE`, `UPDATE_GOAL` were never executed
|
|
114
|
+
by the spider. They will be removed from the enum in 5.4.0.
|
|
115
|
+
|
|
3
116
|
## 5.3.3 — 2026-10
|
|
4
117
|
|
|
5
118
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: graph-crawler
|
|
3
|
-
Version: 5.3.
|
|
3
|
+
Version: 5.3.5
|
|
4
4
|
Summary: Web crawler that builds a graph of a site. HTTP → TLS → browser escalation with pluggable browser engines.
|
|
5
5
|
Author: 0-EternalJunior-0
|
|
6
6
|
Maintainer: 0-EternalJunior-0
|
|
@@ -139,34 +139,20 @@ pip install "graph-crawler[browser]" # + browser (stock Playwright)
|
|
|
139
139
|
pip install "graph-crawler[antidetect]" # + TLS, patchright, CloakBrowser, nodriver
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
|
144
|
-
|
|
|
145
|
-
| `
|
|
146
|
-
| `
|
|
147
|
-
| `
|
|
148
|
-
| `
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
|
|
156
|
-
| extra | what it adds |
|
|
157
|
-
|--------------|---------------------------------------------------------------------|
|
|
158
|
-
| `browser` | `driver="playwright"` (`playwright` is an alias of this extra) |
|
|
159
|
-
| `tls` | `curl_cffi` — TLS fingerprint tier, `driver="tls"` |
|
|
160
|
-
| `stealth` | `patchright` + `playwright-stealth` — engine `"patchright"` |
|
|
161
|
-
| `cloak` | `cloakbrowser` — engine `"cloakbrowser"` (stealth in the binary) |
|
|
162
|
-
| `undetected` | `nodriver` — last tier, `driver="nodriver"` |
|
|
163
|
-
| `antidetect` | the four extras above |
|
|
164
|
-
| `server` | FastAPI dashboard / REST / WebSocket |
|
|
165
|
-
| `text` | `tiktoken` + `stop-words` |
|
|
166
|
-
| `markdown` | `html2md-clean` + `markdown-it-py` — `MarkdownPlugin` |
|
|
167
|
-
| `fuzzy` | `rapidfuzz` — `graph.search(..., mode="fuzzy")` |
|
|
168
|
-
| `mongodb`, `postgresql`, `celery`, `viz`, `articles`, `sitemap-fast`, `performance`, `system`, `all` | storage backends, distributed mode and other extras |
|
|
169
|
-
>>>>>>> d3db85cd9c8826d11c10e33988c837f3e8ffbe88
|
|
142
|
+
| extra | what it adds |
|
|
143
|
+
| -------------- | ------------------------------------------------------------------------------- |
|
|
144
|
+
| `browser` | `driver="playwright"`, stock Playwright (`playwright` is an alias of this extra) |
|
|
145
|
+
| `tls` | `curl_cffi` — TLS fingerprint tier, `driver="tls"` |
|
|
146
|
+
| `stealth` | `patchright` + `playwright-stealth` — engine `"patchright"` |
|
|
147
|
+
| `cloak` | `cloakbrowser` — engine `"cloakbrowser"` (stealth in the binary) |
|
|
148
|
+
| `undetected` | `nodriver` — last tier, `driver="nodriver"` |
|
|
149
|
+
| `antidetect` | the five extras above |
|
|
150
|
+
| `server` | FastAPI dashboard / REST / WebSocket (`graph_crawler.api.server`) |
|
|
151
|
+
| `text` | `tiktoken` + `stop-words` — `count_tokens`, `extract_keywords` |
|
|
152
|
+
| `markdown` | `html2md-clean` + `markdown-it-py` — `MarkdownPlugin` |
|
|
153
|
+
| `fuzzy` | `rapidfuzz` — `graph.search(..., mode="fuzzy")` |
|
|
154
|
+
| `sitemap-fast` | `lxml` sitemap parser |
|
|
155
|
+
| `mongodb`, `postgresql`, `celery`, `viz`, `articles`, `performance`, `system`, `all` | storage backends, distributed mode and other extras |
|
|
170
156
|
|
|
171
157
|
After installing a browser extra, download the browser once:
|
|
172
158
|
|
|
@@ -267,23 +253,13 @@ so `node.ok` is `False` for error pages.
|
|
|
267
253
|
|
|
268
254
|
`driver=` accepts a name or a driver instance.
|
|
269
255
|
|
|
270
|
-
|
|
271
|
-
|
|
|
272
|
-
|
|
|
273
|
-
| `
|
|
274
|
-
| `
|
|
275
|
-
| `
|
|
276
|
-
| `
|
|
277
|
-
| `escalating` | all of them | whatever is installed | http → tls → browser → undetected |
|
|
278
|
-
=======
|
|
279
|
-
| name | tier | needs | notes |
|
|
280
|
-
|--------------|-------------|-----------------------|-------------------------------------------------|
|
|
281
|
-
| `aiohttp` / `http` | http | — | default |
|
|
282
|
-
| `tls` | tls | `[tls]` | Chrome TLS fingerprint, no JavaScript |
|
|
283
|
-
| `playwright` | browser | `[browser]` | real browser; engine chosen by `browser_engine` |
|
|
284
|
-
| `nodriver` | undetected | `[undetected]` | CDP without webdriver |
|
|
285
|
-
| `escalating` | all of them | whatever is installed | http → tls → browser → undetected |
|
|
286
|
-
>>>>>>> d3db85cd9c8826d11c10e33988c837f3e8ffbe88
|
|
256
|
+
| name | tier | needs | notes |
|
|
257
|
+
| ----------------------------- | ----------- | --------------------- | ----------------------------------------------- |
|
|
258
|
+
| `aiohttp` / `http` / `async` | http | — | default |
|
|
259
|
+
| `tls` / `curl_cffi` | tls | `[tls]` | Chrome TLS fingerprint, no JavaScript |
|
|
260
|
+
| `playwright` | browser | `[browser]` | real browser; engine chosen by `browser_engine` |
|
|
261
|
+
| `nodriver` | undetected | `[undetected]` | CDP without webdriver |
|
|
262
|
+
| `escalating` | all of them | whatever is installed | http → tls → browser → undetected |
|
|
287
263
|
|
|
288
264
|
`escalating` creates tiers lazily: a browser starts only when a cheaper tier is
|
|
289
265
|
blocked. Tiers whose dependency is not installed are skipped.
|
|
@@ -299,13 +275,8 @@ Every response goes through `classify_block()`:
|
|
|
299
275
|
| `none` | normal page |
|
|
300
276
|
| `soft_block` | 429, plain 503, or 200 with an empty body |
|
|
301
277
|
| `challenge` | Cloudflare "Just a moment", `cf-mitigated: challenge`, Incapsula, DDoS-Guard |
|
|
302
|
-
<<<<<<< HEAD
|
|
303
|
-
| `hard_block` | 403 "Access Denied", Akamai `_abck`, DataDome headers |
|
|
304
|
-
| `captcha` | PerimeterX, DataDome captcha, reCAPTCHA / hCaptcha / Turnstile markup |
|
|
305
|
-
=======
|
|
306
278
|
| `hard_block` | 403/401/405 **with** a vendor or "Access Denied" marker (Akamai `_abck`, DataDome/Incapsula headers, `x-iinfo`, Cloudflare `server`) |
|
|
307
|
-
| `captcha` | PerimeterX, DataDome captcha, reCAPTCHA / hCaptcha / Turnstile markup
|
|
308
|
-
>>>>>>> d3db85cd9c8826d11c10e33988c837f3e8ffbe88
|
|
279
|
+
| `captcha` | PerimeterX, DataDome captcha, reCAPTCHA / hCaptcha / Turnstile markup |
|
|
309
280
|
|
|
310
281
|
Anything other than `none`, and network errors, move the request to the next tier.
|
|
311
282
|
|
|
@@ -375,25 +346,14 @@ policy = EscalationPolicy(
|
|
|
375
346
|
|
|
376
347
|
### Browser engines
|
|
377
348
|
|
|
378
|
-
|
|
379
|
-
The browser tier is stock `PlaywrightDriver`; the _engine_ decides which
|
|
380
|
-
Playwright module and which Chromium binary it launches.
|
|
381
|
-
|
|
382
|
-
| engine | what it is | falls back to |
|
|
383
|
-
| -------------- | ------------------------------------------------------------- | ------------- |
|
|
384
|
-
| `cloakbrowser` | Chromium with fingerprint patches compiled into the binary | `patchright` |
|
|
385
|
-
| `patchright` | Playwright fork without automation leaks (`Runtime.enable` …) | `playwright` |
|
|
386
|
-
| `playwright` | stock | — |
|
|
387
|
-
=======
|
|
388
|
-
The browser tier is `PlaywrightDriver`; the *engine* decides which Playwright
|
|
349
|
+
The browser tier is `PlaywrightDriver`; the _engine_ decides which Playwright
|
|
389
350
|
module and which Chromium binary it launches.
|
|
390
351
|
|
|
391
352
|
| engine | what it is | falls back to |
|
|
392
|
-
|
|
353
|
+
| -------------- | ------------------------------------------------------------- | ------------- |
|
|
393
354
|
| `cloakbrowser` | Chromium with fingerprint patches compiled into the binary | `patchright` |
|
|
394
355
|
| `patchright` | Playwright fork without automation leaks (`Runtime.enable` …) | `playwright` |
|
|
395
356
|
| `playwright` | stock Playwright | — |
|
|
396
|
-
>>>>>>> d3db85cd9c8826d11c10e33988c837f3e8ffbe88
|
|
397
357
|
|
|
398
358
|
A fallback is logged once at `INFO` with the reason. To get an `ImportError`
|
|
399
359
|
instead of a silent downgrade, set `GC_ENGINE_STRICT=1`, or pass
|
|
@@ -456,6 +416,37 @@ A target is found by `selector`, `text`, `placeholder` or `label` (`nth` picks
|
|
|
456
416
|
among visible matches). The first failing action stops the chain and is written
|
|
457
417
|
to `node.metadata["actions_error"]`.
|
|
458
418
|
|
|
419
|
+
## Controlling a running crawl
|
|
420
|
+
|
|
421
|
+
`async_crawl(..., control_channel=channel)` (and `crawl`, `GraphCrawlerClient.crawl`)
|
|
422
|
+
reads commands from the channel between pages:
|
|
423
|
+
|
|
424
|
+
```python
|
|
425
|
+
import graph_crawler as gc
|
|
426
|
+
from graph_crawler.domain.interfaces import AsyncQueueControlChannel, force_visit_command, stop_command
|
|
427
|
+
|
|
428
|
+
channel = AsyncQueueControlChannel()
|
|
429
|
+
channel.send(force_visit_command("https://example.com/hidden-page"))
|
|
430
|
+
graph = await gc.async_crawl("https://example.com", control_channel=channel)
|
|
431
|
+
|
|
432
|
+
# from a plugin, a callback or another task while the crawl runs:
|
|
433
|
+
channel.send(stop_command("enough data")) # graph.metadata["stop_reason"] == "enough data"
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
| command | `data` | effect |
|
|
437
|
+
| -------------- | ----------------------- | ---------------------------------------------- |
|
|
438
|
+
| `STOP` | `reason` (optional) | stops the crawl, `stop_reason` = `reason` |
|
|
439
|
+
| `PAUSE` | — | pauses until `RESUME` or `STOP` |
|
|
440
|
+
| `RESUME` | — | continues a paused crawl |
|
|
441
|
+
| `REPRIORITIZE` | `url`, `priority` | moves a queued URL in the queue |
|
|
442
|
+
| `FORCE_VISIT` | `url` | adds the URL to the queue with top priority |
|
|
443
|
+
|
|
444
|
+
`SPIDER_COMMANDS` is this set. `SKIP_URL`, `CHANGE_STRATEGY`, `SET_DEPTH_LIMIT`,
|
|
445
|
+
`REQUEST_STATE` and `UPDATE_GOAL` are declared in `CrawlCommand` but not executed:
|
|
446
|
+
the spider logs `WARNING Unsupported control command ...` (once per command type
|
|
447
|
+
per crawl) and goes on; they will be removed in the next minor release. A supported
|
|
448
|
+
command without its required `data` fields is also skipped with a `WARNING`.
|
|
449
|
+
|
|
459
450
|
## Plugins and custom nodes
|
|
460
451
|
|
|
461
452
|
A node plugin runs on every fetched page and writes to `user_data`, `metadata`
|
|
@@ -18,34 +18,20 @@ pip install "graph-crawler[browser]" # + browser (stock Playwright)
|
|
|
18
18
|
pip install "graph-crawler[antidetect]" # + TLS, patchright, CloakBrowser, nodriver
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
|
23
|
-
|
|
|
24
|
-
| `
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
-
| `
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
34
|
-
|
|
35
|
-
| extra | what it adds |
|
|
36
|
-
|--------------|---------------------------------------------------------------------|
|
|
37
|
-
| `browser` | `driver="playwright"` (`playwright` is an alias of this extra) |
|
|
38
|
-
| `tls` | `curl_cffi` — TLS fingerprint tier, `driver="tls"` |
|
|
39
|
-
| `stealth` | `patchright` + `playwright-stealth` — engine `"patchright"` |
|
|
40
|
-
| `cloak` | `cloakbrowser` — engine `"cloakbrowser"` (stealth in the binary) |
|
|
41
|
-
| `undetected` | `nodriver` — last tier, `driver="nodriver"` |
|
|
42
|
-
| `antidetect` | the four extras above |
|
|
43
|
-
| `server` | FastAPI dashboard / REST / WebSocket |
|
|
44
|
-
| `text` | `tiktoken` + `stop-words` |
|
|
45
|
-
| `markdown` | `html2md-clean` + `markdown-it-py` — `MarkdownPlugin` |
|
|
46
|
-
| `fuzzy` | `rapidfuzz` — `graph.search(..., mode="fuzzy")` |
|
|
47
|
-
| `mongodb`, `postgresql`, `celery`, `viz`, `articles`, `sitemap-fast`, `performance`, `system`, `all` | storage backends, distributed mode and other extras |
|
|
48
|
-
>>>>>>> d3db85cd9c8826d11c10e33988c837f3e8ffbe88
|
|
21
|
+
| extra | what it adds |
|
|
22
|
+
| -------------- | ------------------------------------------------------------------------------- |
|
|
23
|
+
| `browser` | `driver="playwright"`, stock Playwright (`playwright` is an alias of this extra) |
|
|
24
|
+
| `tls` | `curl_cffi` — TLS fingerprint tier, `driver="tls"` |
|
|
25
|
+
| `stealth` | `patchright` + `playwright-stealth` — engine `"patchright"` |
|
|
26
|
+
| `cloak` | `cloakbrowser` — engine `"cloakbrowser"` (stealth in the binary) |
|
|
27
|
+
| `undetected` | `nodriver` — last tier, `driver="nodriver"` |
|
|
28
|
+
| `antidetect` | the five extras above |
|
|
29
|
+
| `server` | FastAPI dashboard / REST / WebSocket (`graph_crawler.api.server`) |
|
|
30
|
+
| `text` | `tiktoken` + `stop-words` — `count_tokens`, `extract_keywords` |
|
|
31
|
+
| `markdown` | `html2md-clean` + `markdown-it-py` — `MarkdownPlugin` |
|
|
32
|
+
| `fuzzy` | `rapidfuzz` — `graph.search(..., mode="fuzzy")` |
|
|
33
|
+
| `sitemap-fast` | `lxml` sitemap parser |
|
|
34
|
+
| `mongodb`, `postgresql`, `celery`, `viz`, `articles`, `performance`, `system`, `all` | storage backends, distributed mode and other extras |
|
|
49
35
|
|
|
50
36
|
After installing a browser extra, download the browser once:
|
|
51
37
|
|
|
@@ -146,23 +132,13 @@ so `node.ok` is `False` for error pages.
|
|
|
146
132
|
|
|
147
133
|
`driver=` accepts a name or a driver instance.
|
|
148
134
|
|
|
149
|
-
|
|
150
|
-
|
|
|
151
|
-
|
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
156
|
-
| `escalating` | all of them | whatever is installed | http → tls → browser → undetected |
|
|
157
|
-
=======
|
|
158
|
-
| name | tier | needs | notes |
|
|
159
|
-
|--------------|-------------|-----------------------|-------------------------------------------------|
|
|
160
|
-
| `aiohttp` / `http` | http | — | default |
|
|
161
|
-
| `tls` | tls | `[tls]` | Chrome TLS fingerprint, no JavaScript |
|
|
162
|
-
| `playwright` | browser | `[browser]` | real browser; engine chosen by `browser_engine` |
|
|
163
|
-
| `nodriver` | undetected | `[undetected]` | CDP without webdriver |
|
|
164
|
-
| `escalating` | all of them | whatever is installed | http → tls → browser → undetected |
|
|
165
|
-
>>>>>>> d3db85cd9c8826d11c10e33988c837f3e8ffbe88
|
|
135
|
+
| name | tier | needs | notes |
|
|
136
|
+
| ----------------------------- | ----------- | --------------------- | ----------------------------------------------- |
|
|
137
|
+
| `aiohttp` / `http` / `async` | http | — | default |
|
|
138
|
+
| `tls` / `curl_cffi` | tls | `[tls]` | Chrome TLS fingerprint, no JavaScript |
|
|
139
|
+
| `playwright` | browser | `[browser]` | real browser; engine chosen by `browser_engine` |
|
|
140
|
+
| `nodriver` | undetected | `[undetected]` | CDP without webdriver |
|
|
141
|
+
| `escalating` | all of them | whatever is installed | http → tls → browser → undetected |
|
|
166
142
|
|
|
167
143
|
`escalating` creates tiers lazily: a browser starts only when a cheaper tier is
|
|
168
144
|
blocked. Tiers whose dependency is not installed are skipped.
|
|
@@ -178,13 +154,8 @@ Every response goes through `classify_block()`:
|
|
|
178
154
|
| `none` | normal page |
|
|
179
155
|
| `soft_block` | 429, plain 503, or 200 with an empty body |
|
|
180
156
|
| `challenge` | Cloudflare "Just a moment", `cf-mitigated: challenge`, Incapsula, DDoS-Guard |
|
|
181
|
-
<<<<<<< HEAD
|
|
182
|
-
| `hard_block` | 403 "Access Denied", Akamai `_abck`, DataDome headers |
|
|
183
|
-
| `captcha` | PerimeterX, DataDome captcha, reCAPTCHA / hCaptcha / Turnstile markup |
|
|
184
|
-
=======
|
|
185
157
|
| `hard_block` | 403/401/405 **with** a vendor or "Access Denied" marker (Akamai `_abck`, DataDome/Incapsula headers, `x-iinfo`, Cloudflare `server`) |
|
|
186
|
-
| `captcha` | PerimeterX, DataDome captcha, reCAPTCHA / hCaptcha / Turnstile markup
|
|
187
|
-
>>>>>>> d3db85cd9c8826d11c10e33988c837f3e8ffbe88
|
|
158
|
+
| `captcha` | PerimeterX, DataDome captcha, reCAPTCHA / hCaptcha / Turnstile markup |
|
|
188
159
|
|
|
189
160
|
Anything other than `none`, and network errors, move the request to the next tier.
|
|
190
161
|
|
|
@@ -254,25 +225,14 @@ policy = EscalationPolicy(
|
|
|
254
225
|
|
|
255
226
|
### Browser engines
|
|
256
227
|
|
|
257
|
-
|
|
258
|
-
The browser tier is stock `PlaywrightDriver`; the _engine_ decides which
|
|
259
|
-
Playwright module and which Chromium binary it launches.
|
|
260
|
-
|
|
261
|
-
| engine | what it is | falls back to |
|
|
262
|
-
| -------------- | ------------------------------------------------------------- | ------------- |
|
|
263
|
-
| `cloakbrowser` | Chromium with fingerprint patches compiled into the binary | `patchright` |
|
|
264
|
-
| `patchright` | Playwright fork without automation leaks (`Runtime.enable` …) | `playwright` |
|
|
265
|
-
| `playwright` | stock | — |
|
|
266
|
-
=======
|
|
267
|
-
The browser tier is `PlaywrightDriver`; the *engine* decides which Playwright
|
|
228
|
+
The browser tier is `PlaywrightDriver`; the _engine_ decides which Playwright
|
|
268
229
|
module and which Chromium binary it launches.
|
|
269
230
|
|
|
270
231
|
| engine | what it is | falls back to |
|
|
271
|
-
|
|
232
|
+
| -------------- | ------------------------------------------------------------- | ------------- |
|
|
272
233
|
| `cloakbrowser` | Chromium with fingerprint patches compiled into the binary | `patchright` |
|
|
273
234
|
| `patchright` | Playwright fork without automation leaks (`Runtime.enable` …) | `playwright` |
|
|
274
235
|
| `playwright` | stock Playwright | — |
|
|
275
|
-
>>>>>>> d3db85cd9c8826d11c10e33988c837f3e8ffbe88
|
|
276
236
|
|
|
277
237
|
A fallback is logged once at `INFO` with the reason. To get an `ImportError`
|
|
278
238
|
instead of a silent downgrade, set `GC_ENGINE_STRICT=1`, or pass
|
|
@@ -335,6 +295,37 @@ A target is found by `selector`, `text`, `placeholder` or `label` (`nth` picks
|
|
|
335
295
|
among visible matches). The first failing action stops the chain and is written
|
|
336
296
|
to `node.metadata["actions_error"]`.
|
|
337
297
|
|
|
298
|
+
## Controlling a running crawl
|
|
299
|
+
|
|
300
|
+
`async_crawl(..., control_channel=channel)` (and `crawl`, `GraphCrawlerClient.crawl`)
|
|
301
|
+
reads commands from the channel between pages:
|
|
302
|
+
|
|
303
|
+
```python
|
|
304
|
+
import graph_crawler as gc
|
|
305
|
+
from graph_crawler.domain.interfaces import AsyncQueueControlChannel, force_visit_command, stop_command
|
|
306
|
+
|
|
307
|
+
channel = AsyncQueueControlChannel()
|
|
308
|
+
channel.send(force_visit_command("https://example.com/hidden-page"))
|
|
309
|
+
graph = await gc.async_crawl("https://example.com", control_channel=channel)
|
|
310
|
+
|
|
311
|
+
# from a plugin, a callback or another task while the crawl runs:
|
|
312
|
+
channel.send(stop_command("enough data")) # graph.metadata["stop_reason"] == "enough data"
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
| command | `data` | effect |
|
|
316
|
+
| -------------- | ----------------------- | ---------------------------------------------- |
|
|
317
|
+
| `STOP` | `reason` (optional) | stops the crawl, `stop_reason` = `reason` |
|
|
318
|
+
| `PAUSE` | — | pauses until `RESUME` or `STOP` |
|
|
319
|
+
| `RESUME` | — | continues a paused crawl |
|
|
320
|
+
| `REPRIORITIZE` | `url`, `priority` | moves a queued URL in the queue |
|
|
321
|
+
| `FORCE_VISIT` | `url` | adds the URL to the queue with top priority |
|
|
322
|
+
|
|
323
|
+
`SPIDER_COMMANDS` is this set. `SKIP_URL`, `CHANGE_STRATEGY`, `SET_DEPTH_LIMIT`,
|
|
324
|
+
`REQUEST_STATE` and `UPDATE_GOAL` are declared in `CrawlCommand` but not executed:
|
|
325
|
+
the spider logs `WARNING Unsupported control command ...` (once per command type
|
|
326
|
+
per crawl) and goes on; they will be removed in the next minor release. A supported
|
|
327
|
+
command without its required `data` fields is also skipped with a `WARNING`.
|
|
328
|
+
|
|
338
329
|
## Plugins and custom nodes
|
|
339
330
|
|
|
340
331
|
A node plugin runs on every fetched page and writes to `user_data`, `metadata`
|
|
@@ -68,6 +68,7 @@ async def async_crawl_impl(
|
|
|
68
68
|
worker_pool_size: int | None = None, # A_liffe L3
|
|
69
69
|
per_host_max_in_flight: int | None = None, # ліміт сторінок на хост (None = без ліміту)
|
|
70
70
|
node_enrich_timeout: float | None = None, # per-node таймаут збагачення, с (None = 45 с)
|
|
71
|
+
redirect_host_alias: Any = None, # Callable[[src, dst], bool] — дозволені крос-хост редіректи
|
|
71
72
|
driver: DriverType | None = None,
|
|
72
73
|
driver_config: dict[str, Any] | None = None,
|
|
73
74
|
storage: StorageType | None = None,
|
|
@@ -243,6 +244,7 @@ async def async_crawl_impl(
|
|
|
243
244
|
worker_pool_size=worker_pool_size,
|
|
244
245
|
per_host_max_in_flight=per_host_max_in_flight,
|
|
245
246
|
node_enrich_timeout=node_enrich_timeout,
|
|
247
|
+
redirect_host_alias=redirect_host_alias,
|
|
246
248
|
# Low-memory режим
|
|
247
249
|
low_memory_mode=low_memory_mode,
|
|
248
250
|
evict_threshold=evict_threshold,
|
|
@@ -56,6 +56,7 @@ async def async_crawl(
|
|
|
56
56
|
worker_pool_size: int | None = None, # A_liffe L3
|
|
57
57
|
per_host_max_in_flight: int | None = None,
|
|
58
58
|
node_enrich_timeout: float | None = None,
|
|
59
|
+
redirect_host_alias: Any = None,
|
|
59
60
|
driver: DriverType | None = None,
|
|
60
61
|
driver_config: dict[str, Any] | None = None,
|
|
61
62
|
storage: StorageType | None = None,
|
|
@@ -134,6 +135,7 @@ async def async_crawl(
|
|
|
134
135
|
worker_pool_size=worker_pool_size,
|
|
135
136
|
per_host_max_in_flight=per_host_max_in_flight,
|
|
136
137
|
node_enrich_timeout=node_enrich_timeout,
|
|
138
|
+
redirect_host_alias=redirect_host_alias,
|
|
137
139
|
driver=driver,
|
|
138
140
|
driver_config=driver_config,
|
|
139
141
|
storage=storage,
|
|
@@ -130,6 +130,8 @@ class GraphCrawlerClient:
|
|
|
130
130
|
per_host_max_in_flight: int | None = None,
|
|
131
131
|
# Per-node таймаут збагачення, с (None = DEFAULT_NODE_ENRICH_TIMEOUT)
|
|
132
132
|
node_enrich_timeout: float | None = None,
|
|
133
|
+
# Callable[[source_url, target_url], bool] — дозволені крос-хост редіректи
|
|
134
|
+
redirect_host_alias: Any = None,
|
|
133
135
|
# Low-memory режим
|
|
134
136
|
low_memory_mode: bool = False,
|
|
135
137
|
evict_threshold: int = 500,
|
|
@@ -195,6 +197,7 @@ class GraphCrawlerClient:
|
|
|
195
197
|
worker_pool_size=worker_pool_size,
|
|
196
198
|
per_host_max_in_flight=per_host_max_in_flight,
|
|
197
199
|
node_enrich_timeout=node_enrich_timeout,
|
|
200
|
+
redirect_host_alias=redirect_host_alias,
|
|
198
201
|
# Low-memory режим
|
|
199
202
|
low_memory_mode=low_memory_mode,
|
|
200
203
|
evict_threshold=evict_threshold,
|
|
@@ -50,6 +50,7 @@ def crawl(
|
|
|
50
50
|
worker_pool_size: int | None = None,
|
|
51
51
|
per_host_max_in_flight: int | None = None,
|
|
52
52
|
node_enrich_timeout: float | None = None,
|
|
53
|
+
redirect_host_alias: Any = None,
|
|
53
54
|
driver: DriverType | None = None,
|
|
54
55
|
driver_config: dict[str, Any] | None = None,
|
|
55
56
|
storage: StorageType | None = None,
|
|
@@ -98,6 +99,8 @@ def crawl(
|
|
|
98
99
|
per_host_max_in_flight: ліміт одночасних запитів на хост (``None`` — без ліміту).
|
|
99
100
|
node_enrich_timeout: ліміт збагачення однієї ноди, с (``None`` — 45 с); при перевищенні
|
|
100
101
|
нода стає ERROR, лінки, витягнуті до таймауту, лишаються в графі.
|
|
102
|
+
redirect_host_alias: ``Callable[[source_url, target_url], bool]`` — дозволяє редірект
|
|
103
|
+
на хост поза ``allowed_domains`` (напр. між хостами однієї ATS); ``None`` — ні.
|
|
101
104
|
driver: драйвер завантаження (``"http"``, ``"playwright"``, ``"escalating"`` тощо).
|
|
102
105
|
driver_config: налаштування драйвера.
|
|
103
106
|
storage: тип сховища (``"memory"``, ``"json"``, ``"sqlite"`` тощо).
|
|
@@ -178,6 +181,7 @@ def crawl(
|
|
|
178
181
|
"worker_pool_size": worker_pool_size,
|
|
179
182
|
"per_host_max_in_flight": per_host_max_in_flight,
|
|
180
183
|
"node_enrich_timeout": node_enrich_timeout,
|
|
184
|
+
"redirect_host_alias": redirect_host_alias,
|
|
181
185
|
"driver": driver,
|
|
182
186
|
"driver_config": driver_config,
|
|
183
187
|
"storage": storage,
|
|
@@ -1003,11 +1003,30 @@ class CrawlCoordinator:
|
|
|
1003
1003
|
# Захист від cross-domain редіректів повз same_domain фільтр:
|
|
1004
1004
|
# драйвер може прозоро фоловити Location: на чужий домен, тож
|
|
1005
1005
|
# питаємо domain_filter перед тим як зафіксувати редірект у графі.
|
|
1006
|
+
#
|
|
1007
|
+
# P21: редірект на ТОЙ САМИЙ хост дозволяємо завжди (коротка форма ATS
|
|
1008
|
+
# `apply.workable.com/j/X` → `apply.workable.com/<slug>/j/X`). Для різних
|
|
1009
|
+
# хостів — domain_filter; якщо він відмовив, пробуємо точку розширення
|
|
1010
|
+
# «аліаси хостів» (`allow_redirect`), якою Parser передає свої
|
|
1011
|
+
# DOMAIN_POLICIES (`boards.greenhouse.io` → `job-boards.greenhouse.io`).
|
|
1006
1012
|
try:
|
|
1013
|
+
from urllib.parse import urlsplit
|
|
1014
|
+
|
|
1015
|
+
src_host = urlsplit(original_node.url).hostname or ""
|
|
1016
|
+
tgt_host = urlsplit(final_url).hostname or ""
|
|
1017
|
+
same_host = bool(src_host) and src_host == tgt_host
|
|
1018
|
+
|
|
1007
1019
|
domain_filter = getattr(self.processor, "domain_filter", None)
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1020
|
+
blocked = False
|
|
1021
|
+
if not same_host and domain_filter is not None:
|
|
1022
|
+
if not domain_filter.is_allowed(final_url, source_url=original_node.url):
|
|
1023
|
+
alias_hook = getattr(domain_filter, "allow_redirect", None)
|
|
1024
|
+
allowed_by_alias = bool(
|
|
1025
|
+
alias_hook and alias_hook(original_node.url, final_url)
|
|
1026
|
+
)
|
|
1027
|
+
blocked = not allowed_by_alias
|
|
1028
|
+
|
|
1029
|
+
if blocked:
|
|
1011
1030
|
logger.info(
|
|
1012
1031
|
"Redirect blocked by domain_filter: %s -> %s (cross-domain or disallowed)",
|
|
1013
1032
|
original_node.url,
|
|
@@ -47,6 +47,13 @@ class DomainFilter(BaseURLFilter):
|
|
|
47
47
|
super().__init__(config.model_dump() if hasattr(config, "model_dump") else dict(config))
|
|
48
48
|
self.event_bus = event_bus
|
|
49
49
|
|
|
50
|
+
# P21: точка розширення «аліаси хостів». Callable[[source_url, target_url], bool].
|
|
51
|
+
# Коли редірект веде на хост поза allowed_domains, координатор питає цей
|
|
52
|
+
# хук — так Parser дозволяє редірект між хостами однієї ATS-політики
|
|
53
|
+
# (boards.greenhouse.io → job-boards.greenhouse.io), не відкриваючи весь
|
|
54
|
+
# інтернет. None → редірект за межі allowed_domains блокується, як раніше.
|
|
55
|
+
self.redirect_host_alias = None
|
|
56
|
+
|
|
50
57
|
# Парсимо спеціальні патерни
|
|
51
58
|
self._parse_special_patterns()
|
|
52
59
|
|
|
@@ -92,6 +99,30 @@ class DomainFilter(BaseURLFilter):
|
|
|
92
99
|
def name(self) -> str:
|
|
93
100
|
return "domain"
|
|
94
101
|
|
|
102
|
+
def allow_redirect(self, source_url: str, target_url: str) -> bool:
|
|
103
|
+
"""Точка розширення P21: чи дозволити редірект на хост поза allowed_domains.
|
|
104
|
+
|
|
105
|
+
Координатор завжди дозволяє редірект на той самий хост сам; сюди
|
|
106
|
+
потрапляють лише крос-хостові редіректи, які не пройшли ``is_allowed``.
|
|
107
|
+
За замовчуванням ``False``; якщо встановлено ``redirect_host_alias``
|
|
108
|
+
(Parser передає свої DOMAIN_POLICIES) — делегуємо рішення йому.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
source_url: URL, що редіректив.
|
|
112
|
+
target_url: Фінальний URL редіректу.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
True, якщо редірект на цей чужий хост дозволено як аліас.
|
|
116
|
+
"""
|
|
117
|
+
hook = self.redirect_host_alias
|
|
118
|
+
if hook is None:
|
|
119
|
+
return False
|
|
120
|
+
try:
|
|
121
|
+
return bool(hook(source_url, target_url))
|
|
122
|
+
except Exception as exc: # noqa: BLE001 — хук не повинен валити обхід
|
|
123
|
+
logger.warning("redirect_host_alias hook failed for %s -> %s: %s", source_url, target_url, exc)
|
|
124
|
+
return False
|
|
125
|
+
|
|
95
126
|
def _is_subdomain_of(self, domain: str, base_domain: str) -> bool:
|
|
96
127
|
"""
|
|
97
128
|
Перевіряє чи domain є субдоменом base_domain або самим base_domain.
|