graph-crawler 5.3.4__tar.gz → 5.3.6__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.4 → graph_crawler-5.3.6}/CHANGELOG.md +100 -0
- {graph_crawler-5.3.4/graph_crawler.egg-info → graph_crawler-5.3.6}/PKG-INFO +1 -1
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/_core.py +2 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/async_.py +2 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/client/client.py +3 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/sync.py +4 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/mappers/node_mapper.py +1 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/crawl_coordinator.py +22 -3
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/filters/domain_filter.py +31 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/sitemap_parser.py +150 -14
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/sitemap_processor.py +3 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/sitemap_spider.py +48 -3
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/spider.py +3 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/graph.py +129 -7
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/node.py +87 -11
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/sitemap_node.py +3 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/configs.py +3 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/sqlite_eviction_storage.py +1 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/escalation.py +128 -19
- {graph_crawler-5.3.4 → graph_crawler-5.3.6/graph_crawler.egg-info}/PKG-INFO +1 -1
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/pyproject.toml +1 -1
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/LICENSE +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/MANIFEST.in +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/README.md +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/__version__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/ai/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/_distributed.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/_shared.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/_sitemap_distributed.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/cli.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/client/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/history_manager.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/project_init.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/server/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/server/crawl_monitor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/server/dashboard.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/server/rest_api.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/server/webhooks.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/server/websocket_manager.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/stats_collector.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/bootstrap.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/context/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/context/dependency_registry.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/context/graph_context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/context/merge_context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/edge_dto.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/graph_dto.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/mappers/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/mappers/edge_mapper.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/mappers/graph_mapper.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/mappers/node_data_codec.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/node_dto.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/utils.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/application_container.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/driver_factory.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/base_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/csv_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/edge_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/excel_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/jsonl_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/node_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/parquet_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/sql_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/finalize.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/low_memory_graph_saver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/resource_budget.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/scheduling/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/scheduling/rule_based_scheduler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/storage_factory.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/adaptive_throttler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/async_bridge.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/base_spider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/celery_batch_spider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/celery_spider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/checkpoint.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/dead_letter_queue.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/domain_rate_limiter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/filters/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/filters/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/filters/domain_patterns.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/filters/path_filter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/incremental_strategy.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/link_processor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/multiprocess_spider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/node_scanner.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/parsers/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/parsers/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/parsers/html_parser.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/progress_tracker.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/scheduler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/serialization_mixin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/spider_lifecycle.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/worker_pool.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/graph_export.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/graph_merge/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/graph_merge/graph_operations.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/graph_merge/merge_strategies.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/data/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/data/backends/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/data/backends/memory.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/data/backends/sqlite.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/data/interfaces.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/context/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/context/crawl_context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/edge.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/edge_analysis.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/graph_operations.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/graph_statistics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/merge_strategies.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/redirect_mode.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/registries.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/strategies.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/crawl.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/edge.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/event_bus.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/events.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/fetch.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/idempotent_subscriber.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/legacy_type_map.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/llm.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/node.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/scheduler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/adapter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/control_channel.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/crawl_context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/distributed_spider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/driver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/event_bus.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/exporters.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/filter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/merge_context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/node_interfaces.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/parser.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/plugin_manager.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/processor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/scanner.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/scheduler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/scheduling/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/scheduling/frontier.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/scheduling/scheduler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/spider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/stop_condition.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/storage/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/storage/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/storage/eviction.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/storage/queue.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/adjacency_maintenance.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/cycle_detection.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/degree_ranking.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/depth_context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/display_title.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/duplicate_detection.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/duplicate_selector.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/duplicate_stats.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/edge_filter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/edge_statistics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/eviction_policy.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/graph_query_cascade.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/graph_statistics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/graph_traversal.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/language_versions.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/lsh_index.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/minimap_builder.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/node_content_resolver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/node_view_builder.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/pagination_guards.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/predicate_ops.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/schema_filter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/search_haystack.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/simhash_clustering.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/simhash_fallback.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/similarity_ranking.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/text_search.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/url_clustering.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/url_deduplication.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/url_normalizer.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/url_rule_matcher.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/url_tree_builder.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/domain_patterns.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/fetch_options.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/html_document.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/lifecycle.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/models.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/page_actions.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/redirect_hop.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/scheduling/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/scheduling/crawl_request.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/scheduling/retry_policy.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/settings.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/url_normalization.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/views.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/cache_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/chain.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/error_recovery_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/logging_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/proxy_health.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/proxy_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/proxy_models.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/proxy_selection.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/rate_limit_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/request_response_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/retry_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/robots_cache.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/robots_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/robots_validator.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/user_agent_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/builtin/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/builtin/stats_export_plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/crawl_engine/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/crawl_engine/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/crawl_engine/priority_provider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/crawl_engine/smart_crawl.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/anti_bot_detection.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/anti_bot_playwright.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/anti_bot_scripts.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/anti_bot_stealth.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/captcha/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/captcha/detector.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/captcha/models.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/captcha/plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/captcha/services.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/content_extractors/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/content_extractors/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/content_extractors/goose3_extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/content_extractors/newspaper_extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/content_extractors/plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/content_extractors/readability_extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/defaults.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/extractors/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/extractors/email_extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/extractors/phone_extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/extractors/price_extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/links.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/markdown_plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/metadata.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/smart_page_finder.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/constants.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/exceptions.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/options.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/jsonld.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/microdata.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/opengraph.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/rdfa.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/twitter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/result.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/text.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/url_normalizer.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/registry.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/adapters/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/adapters/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/adapters/selectolax_adapter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/celery_app.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/celery_batch.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/celery_job_task.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/celery_unified.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/config.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/easy_crawler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/worker_api.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/auto_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/eviction.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/graph_repository.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/json/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/json_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/lmdb_eviction_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/memory/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/memory_dedup_store.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/memory_frontier.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/memory_politeness_store.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/memory_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/mongodb_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/naming_strategy.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/postgresql_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/protocols/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/redis_dedup_store.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/redis_politeness_store.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/repository.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/sqlite/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/sqlite_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/file_job_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/file_queue_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/memory_job_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/memory_queue_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/postgresql_job_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/postgresql_queue_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/unified_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/config.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/driver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/driver_curl_cffi.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/autothrottle.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/autothrottle_adaptive.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/headers.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/http_cache.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/rate_limiter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/retry.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/stages.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/base_plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/compression/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/compression/strategy.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/connection_pool.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/core/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/core/base_async.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/core/body_decoder.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/core/mixins.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/core/redirect_utils.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/factory.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/adaptive_semaphore.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/batch_scheduler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/browser_factory.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/browser_profile.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/config.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/driver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/engine.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/fetch_metrics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/net/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/net/geoip.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/net/proxy_pool.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/net/rate_limiter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/page_actions.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/page_fetch.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/_network_record.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/captcha_detector.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/captcha_solver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/cookie_consent.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/form_filler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/human_bezier.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/network_log.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/ram_adaptive.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/screenshot.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/user_profile.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/pool_lifecycle.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/pooled_driver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/stages.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/stealth_hints.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/plugin_manager.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/protocols.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/undetected/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/undetected/async_driver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/native/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/native/bloom_filter.pyx +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/native/html_parser.pyx +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/native/setup.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/native/url_utils.pyx +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/decorators/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/decorators/cache.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/decorators/log.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/decorators/retry.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/decorators/timing.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/error_tracing.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/extensions/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/extensions/memory_guard.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/extensions/state_manager.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/base_metrics_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/crawl_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/dlq_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/error_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/logging_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/metrics_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/node_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/plugin_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/storage_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/url_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/metrics/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/metrics/memory_profiler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/metrics/metrics_collector.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/metrics/prometheus_metrics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/metrics_core.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/structured_logging.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/tracing.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/optimizations/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/optimizations/generators.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/optimizations/memory_slots.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/optimizations/simhash_numba.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/py.typed +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/constants.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/dto/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/dto/edge_dto.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/dto/graph_dto.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/dto/node_dto.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/error_handling/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/error_handling/error_handler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/exceptions.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/platform/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/platform/adaptive_resolver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/platform/config.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/platform/detector.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/platform/diagnostics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/platform/metrics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/security/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/security/url_sanitizer.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/security/url_validator.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/async_compat.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/bloom_filter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/captcha/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/captcha/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/captcha/manager.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/celery_config.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/celery_helpers.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/cgroup_limits.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/content_decoder.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/distributed_rate_limiter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/distributed_rate_limiter_backends.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/dns_cache.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/enterprise_ua_generator.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/event_publisher_mixin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/fast_json.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/fingerprint.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/fingerprint_data.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/fingerprint_generators.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/fingerprint_profile.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/fingerprint_stealth.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/html_utils.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/id_generator.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/isolated_executor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/memory_optimizer.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/rate_limiter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/regex_validator.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/shadow_dom.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/text_utils.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/url_patterns.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/url_utils.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/user_agent_rotator.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/validation_helpers.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/visualization.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/visualization_core.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/tools/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/tools/db_cleanup.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler.egg-info/SOURCES.txt +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler.egg-info/dependency_links.txt +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler.egg-info/entry_points.txt +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler.egg-info/requires.txt +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler.egg-info/top_level.txt +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.6}/setup.cfg +0 -0
|
@@ -1,5 +1,105 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 5.3.6 — 2026-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **A timeout on robots.txt / sitemap stops the sitemap phase of the host (1.6, daxx.com).** A down origin behind
|
|
10
|
+
Cloudflare does not always answer 52x: robots.txt came back after ~20 s and every sitemap request hung until the
|
|
11
|
+
client timeout, then the default candidates (`sitemap.xml`, `sitemap_index.xml`) and the driver's retries/tier
|
|
12
|
+
escalation repeated it (80–180 s per domain). Now:
|
|
13
|
+
- `SitemapParser._fetch` bounds the whole request by `SitemapParser.timeout` (covers driver retries and tier
|
|
14
|
+
escalation); a timeout is an answer "origin does not respond", not a reason to try again;
|
|
15
|
+
- the first timeout (no status + timeout error, incl. the driver's `tier timeout after …`) or CF 520–527 marks the
|
|
16
|
+
host `ORIGIN_DOWN` (`SitemapParser.mark_origin_down` / `origin_down_reason`); no further request to that host in
|
|
17
|
+
this sitemap phase — no default candidates, no nested sitemaps, no retries;
|
|
18
|
+
- `result["origin_down_reason"]` = `"timeout"` | `"cf_52x"`; `SitemapSpider.origin_down`,
|
|
19
|
+
`get_stats()["origin_down"]`, `SITEMAP_CRAWL_COMPLETED.data["origin_down"]`, and the robots.txt node gets
|
|
20
|
+
`error_message = "origin_down: <reason>"`. A spider-level `SITEMAP_PARSE_TIMEOUT` also marks the host down.
|
|
21
|
+
- Non-timeout network errors (DNS, connection refused) keep the old behaviour (default candidates are tried).
|
|
22
|
+
Live: daxx.com sitemap phase 80 s → 50 s (robots ~20 s + one 30 s timeout), healthy sites unchanged.
|
|
23
|
+
- Tests: `tests/test_sitemap_origin_down_timeout.py` (8).
|
|
24
|
+
|
|
25
|
+
## 5.3.5 — 2026-06
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- **Dirty `Sitemap:` value in robots.txt no longer loses the sitemap (P19).** `SitemapParser._parse_robots_txt`
|
|
30
|
+
took the whole directive value verbatim (`line[8:].strip()`), so `Sitemap: sitemap URL https://site.com/sitemap_index.xml`
|
|
31
|
+
was glued into a broken address and the sitemap was lost. It now extracts the first `https?://\S+` from the value;
|
|
32
|
+
`urljoin` is applied only when the value is a relative path starting with `/`. New `SitemapParser._clean_sitemap_value`.
|
|
33
|
+
- **A 404/410 is never a bot block (P19 side-finding).** `escalation._classify` returns `BotBlockStatus.NONE` for
|
|
34
|
+
status 404/410 right after the authoritative challenge headers. A dead sitemap candidate (or any not-found page) is
|
|
35
|
+
no longer classified as `captcha`/`challenge` and dragged through every tier (http→tls→browser). A real challenge
|
|
36
|
+
uses 403/503, not 404, so the authoritative header check still wins. Invariant: 404 is a definitive "not found"
|
|
37
|
+
from the server, not a page served instead of content. (After N3, verify & lock with a test — see Parser side.)
|
|
38
|
+
- **A redirect inside the same ATS no longer blocks `domain_filter` (P21).** `CrawlCoordinator._handle_redirect_after_scan`
|
|
39
|
+
always allows a redirect to the **same host** (short ATS forms `apply.workable.com/j/X` → `apply.workable.com/<slug>/j/X`).
|
|
40
|
+
For a different host it still asks `domain_filter.is_allowed`, and when that refuses it consults a new host-alias
|
|
41
|
+
extension point `DomainFilter.allow_redirect(source, target)` (driven by a settable `redirect_host_alias` hook) — so
|
|
42
|
+
the Parser can pass its `DOMAIN_POLICIES` and allow `boards.greenhouse.io` → `job-boards.greenhouse.io` without
|
|
43
|
+
opening the whole internet.
|
|
44
|
+
- **Sitemap under a Cloudflare 52x origin error stops without retries (1.6).** On the first `520`–`527` response to
|
|
45
|
+
robots.txt / sitemap, `SitemapParser` stops the sitemap phase and returns `result["origin_down"] = True` (logged as
|
|
46
|
+
`ORIGIN_DOWN`) instead of burning 100–180 s on retries/escalation against a down origin. New
|
|
47
|
+
`SitemapParser._is_origin_down`; `origin_down` is propagated through robots / default / nested sitemap paths.
|
|
48
|
+
- **Nodes evicted by low-memory eviction are no longer lost in a browser full rescan (P23).**
|
|
49
|
+
`Graph.iter_nodes_async(include_evicted=True)` now also yields nodes that were evicted to disk (read-only, without
|
|
50
|
+
pulling them back into RAM), so a merge that iterates the source graph sees them. `Node.pinned` + `Graph.pin_node()`
|
|
51
|
+
/ `Graph.pin_seed_nodes()` keep seed (`depth == 0`) vacancies in RAM — they were the oldest and got evicted first.
|
|
52
|
+
`Graph.eviction_stats` exposes the graph's own eviction counters (a backend-less graph evicts to LMDB/SQLite, which
|
|
53
|
+
`backend.get_stats()` reported as 0).
|
|
54
|
+
- **A 403 storm without vendor markers is treated as a block (P24).** `EscalatingDriver` tracks a per-host window of
|
|
55
|
+
recent HTTP statuses; if a host that recently returned 2xx then returns 403 with no anti-bot markers and either ≥3
|
|
56
|
+
consecutive 403 or >30 % 403 in the window, the 403 is reclassified as `SOFT_BLOCK` (`escalation_reason=http_403_storm`)
|
|
57
|
+
and the tier escalates. A single 403 on a 2xx background is still treated as auth/method (no escalation). New
|
|
58
|
+
`_HostStatusWindow`.
|
|
59
|
+
- **The HTML enrichment pool is priority-aware (2.1 / §15.4).** `run_in_html_pool(fn, *args, priority=...)` admits the
|
|
60
|
+
highest-priority waiter first when the pool is saturated (job-URLs and career subdomains enrich before catalog/blog
|
|
61
|
+
pages); with a free slot and empty queue it behaves exactly like the old FIFO semaphore. `Node` passes its URL-rule
|
|
62
|
+
priority. New `_PrioritySemaphore`.
|
|
63
|
+
- **Cloudflare 520–527 is never a bot block.** `escalation._classify` returns `NONE` for 520–527 (as for 404/410):
|
|
64
|
+
the CF error page embeds `challenge-platform`, so a down origin was classified `challenge` and escalated
|
|
65
|
+
http→tls→browser (~20 s per tier) before `SitemapParser` could stop on `ORIGIN_DOWN`.
|
|
66
|
+
- **robots.txt after a domain move keeps the new host's sitemap.** The same-origin gate and relative `Sitemap:`
|
|
67
|
+
paths now use the host that actually served robots.txt (`final_url`): `kilo.health` → `kilo.co/robots.txt` with
|
|
68
|
+
`Sitemap: … https://kilo.co/sitemap_index.xml` was dropped as cross-origin even after P19 cleaning.
|
|
69
|
+
- **Cancelling a call waiting in the HTML-pool queue resumes the enrich budget.** With the priority semaphore the
|
|
70
|
+
acquire moved out of the `try`, so a cancellation in the queue left the node deadline suspended.
|
|
71
|
+
- **`Node.pinned` does not leak into `user_data['_custom_fields']`** (`NodeMapper`, SQLite eviction storage base
|
|
72
|
+
fields) — it broke DTO/user-data invariants (`test_reserved_user_data_keys`, `test_node_mapper_content_depth`).
|
|
73
|
+
- Tests: `tests/test_v535_fixes.py` (28, offline: P19, 404/52x, 1.6, P21 incl. end-to-end `async_crawl`, P23, P24, 2.1).
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
|
|
77
|
+
- `crawl` / `async_crawl` / `GraphCrawlerClient.crawl` / `CrawlerConfig`: `redirect_host_alias=` —
|
|
78
|
+
`Callable[[source_url, target_url], bool]` installed on the spider's `DomainFilter.redirect_host_alias`
|
|
79
|
+
(the extension point was otherwise unreachable through the public API).
|
|
80
|
+
- `SitemapParser._clean_sitemap_value`, `SitemapParser._is_origin_down`; `result["origin_down"]` key.
|
|
81
|
+
- `DomainFilter.allow_redirect(source_url, target_url)` and settable `DomainFilter.redirect_host_alias` hook.
|
|
82
|
+
- `Node.pinned`; `Graph.pin_node`, `Graph.pin_seed_nodes`, `Graph.eviction_stats`; `iter_nodes_async(include_evicted=)`.
|
|
83
|
+
- `run_in_html_pool(..., priority=)`.
|
|
84
|
+
|
|
85
|
+
### Fixed (carried from Unreleased)
|
|
86
|
+
|
|
87
|
+
- **Sitemap `<lastmod>` is no longer dropped.** `SitemapParser` reads `<lastmod>` of every `<url>` (with or without
|
|
88
|
+
namespace) into `result["lastmod"]` (`{url: value}`, merged across robots/default/nested sitemaps), and
|
|
89
|
+
`SitemapProcessor.create_url_nodes(lastmod=...)` stores it on the new `SitemapNode.lastmod` field (W3C datetime as
|
|
90
|
+
in the file). Consumers can now tell a 2021 post from a fresh page; `_parse_urlset` keeps its contract.
|
|
91
|
+
|
|
92
|
+
- **An embedded captcha widget on a page with content is not a bot block.** `classify_block` follows the same
|
|
93
|
+
principle as the browser `CaptchaDetectorPlugin`: a block is a page served *instead of* content. After the
|
|
94
|
+
authoritative challenge headers (`cf-mitigated`, `x-amzn-waf-action`, `x-vercel-mitigated`), a non-blocking
|
|
95
|
+
response whose `<body>` has ≥1000 visible characters or ≥15 links is `NONE`, whatever widget or protection
|
|
96
|
+
script it embeds. Previously visible text was measured over the first 64 KB of the document (in WordPress that is
|
|
97
|
+
almost all `<head>`), and `cf-turnstile` / `challenge-platform` (Cloudflare loads its JSD script on regular pages)
|
|
98
|
+
were unconditional markers — so every page with reCAPTCHA/hCaptcha/Turnstile in a contact or apply form was
|
|
99
|
+
`captcha`, escalated HTTP→TLS and pinned the whole host to the TLS tier. `cf-turnstile`, `captcha-container`,
|
|
100
|
+
`challenge-platform`, `_incapsula_resource`, `awswafintegration` now count only on pages without content or with a
|
|
101
|
+
blocking status; markers and text are read from `<body>` up to 1 MB.
|
|
102
|
+
|
|
3
103
|
## 5.3.4 — 2026-10
|
|
4
104
|
|
|
5
105
|
### Fixed
|
|
@@ -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.
|
|
@@ -15,6 +15,7 @@ import asyncio
|
|
|
15
15
|
import gzip
|
|
16
16
|
import logging
|
|
17
17
|
import os
|
|
18
|
+
import re
|
|
18
19
|
from concurrent.futures import ThreadPoolExecutor
|
|
19
20
|
from dataclasses import dataclass, field
|
|
20
21
|
from typing import TYPE_CHECKING
|
|
@@ -63,6 +64,12 @@ _MSG_UNKNOWN_SITEMAP_TYPE = "Unknown sitemap type: %s"
|
|
|
63
64
|
# тому такі файли читаються напряму через aiohttp навіть за наявності драйвера.
|
|
64
65
|
_BINARY_CONTENT_TYPES = ("gzip", "octet-stream")
|
|
65
66
|
|
|
67
|
+
# Причини ORIGIN_DOWN: origin не відповідає — sitemap-фаза хоста зупиняється без ретраїв.
|
|
68
|
+
ORIGIN_DOWN_CF_5XX = "cf_52x"
|
|
69
|
+
ORIGIN_DOWN_TIMEOUT = "timeout"
|
|
70
|
+
# Як різні транспорти формулюють таймаут у ``FetchResponse.error`` (aiohttp, curl_cffi, тир-ланцюг).
|
|
71
|
+
_TIMEOUT_ERROR_MARKERS = ("timeout", "timed out")
|
|
72
|
+
|
|
66
73
|
|
|
67
74
|
@dataclass
|
|
68
75
|
class _Fetched:
|
|
@@ -153,6 +160,10 @@ class SitemapParser:
|
|
|
153
160
|
# щоб уникнути шуму, коли той самий сайт запитується декілька разів.
|
|
154
161
|
self._logged_domain_redirects: set[tuple[str, str]] = set()
|
|
155
162
|
|
|
163
|
+
# Хости з лежачим origin (CF 52x або таймаут запиту): host → причина. Перший такий
|
|
164
|
+
# сигнал зупиняє sitemap-фазу хоста — далі жодного запиту, ні ретраю, ні кандидата.
|
|
165
|
+
self._down_hosts: dict[str, str] = {}
|
|
166
|
+
|
|
156
167
|
self._xml_executor = ThreadPoolExecutor(
|
|
157
168
|
max_workers=4, thread_name_prefix=f"xml_parser_{id(self)}_"
|
|
158
169
|
)
|
|
@@ -190,6 +201,18 @@ class SitemapParser:
|
|
|
190
201
|
return url.lower().endswith(".gz") or any(t in content_type for t in _BINARY_CONTENT_TYPES)
|
|
191
202
|
|
|
192
203
|
async def _fetch(self, url: str) -> _Fetched:
|
|
204
|
+
"""robots.txt/sitemap з верхньою межею ``self.timeout`` на весь запит.
|
|
205
|
+
|
|
206
|
+
Межа покриває і ретраї, і ескалацію тирів драйвера: таймаут — це вже відповідь
|
|
207
|
+
«origin не відповідає», а не привід пробувати ще раз (див. ``ORIGIN_DOWN``).
|
|
208
|
+
"""
|
|
209
|
+
try:
|
|
210
|
+
async with asyncio.timeout(self.timeout):
|
|
211
|
+
return await self._fetch_unbounded(url)
|
|
212
|
+
except TimeoutError:
|
|
213
|
+
return _Fetched(status=None, final_url=url, error=f"timeout after {self.timeout}s")
|
|
214
|
+
|
|
215
|
+
async def _fetch_unbounded(self, url: str) -> _Fetched:
|
|
193
216
|
"""robots.txt/sitemap через ``IDriver`` (тир-ланцюг) або власний aiohttp.
|
|
194
217
|
|
|
195
218
|
``FetchResponse.html`` — текст: бінарні ``.gz`` (за URL або Content-Type)
|
|
@@ -262,11 +285,16 @@ class SitemapParser:
|
|
|
262
285
|
- ``sitemap_indexes``: sitemap-index URL (якщо є).
|
|
263
286
|
"""
|
|
264
287
|
result = {"sitemap_urls": [], "urls": [], "sitemap_indexes": []}
|
|
288
|
+
robots_url = urljoin(base_url, "/robots.txt")
|
|
289
|
+
if self._mark_if_origin_down_cached(robots_url, result):
|
|
290
|
+
return result
|
|
265
291
|
|
|
266
292
|
try:
|
|
267
|
-
robots_url = urljoin(base_url, "/robots.txt")
|
|
268
293
|
fetched = await self._fetch(robots_url)
|
|
269
294
|
|
|
295
|
+
if self._check_origin(robots_url, fetched):
|
|
296
|
+
return self._with_origin_down(robots_url, result)
|
|
297
|
+
|
|
270
298
|
if fetched.status != 200 or fetched.body is None:
|
|
271
299
|
logger.warning("robots.txt not found at %s", robots_url)
|
|
272
300
|
return await self._try_default_sitemaps_async(base_url, result)
|
|
@@ -275,7 +303,10 @@ class SitemapParser:
|
|
|
275
303
|
body = fetched.body
|
|
276
304
|
robots_content = body if isinstance(body, str) else body.decode("utf-8", "replace")
|
|
277
305
|
|
|
278
|
-
|
|
306
|
+
# Origin і база відносних шляхів — хост, що реально віддав robots.txt:
|
|
307
|
+
# після переїзду домену (kilo.health → kilo.co) його Sitemap: вказує на новий хост.
|
|
308
|
+
robots_base = fetched.final_url or base_url
|
|
309
|
+
sitemap_urls = self._parse_robots_txt(robots_content, robots_base)
|
|
279
310
|
|
|
280
311
|
if sitemap_urls:
|
|
281
312
|
result["sitemap_urls"] = sitemap_urls
|
|
@@ -290,15 +321,20 @@ class SitemapParser:
|
|
|
290
321
|
logger.warning("Sitemap parse error: %s", sitemap_data)
|
|
291
322
|
continue
|
|
292
323
|
if isinstance(sitemap_data, dict):
|
|
324
|
+
if sitemap_data.get("origin_down"):
|
|
325
|
+
result["origin_down"] = True
|
|
326
|
+
result["origin_down_reason"] = sitemap_data.get("origin_down_reason")
|
|
293
327
|
result["urls"].extend(sitemap_data.get("urls", []))
|
|
294
328
|
result["sitemap_indexes"].extend(sitemap_data.get("sitemap_indexes", []))
|
|
329
|
+
result.setdefault("lastmod", {}).update(sitemap_data.get("lastmod", {}))
|
|
295
330
|
else:
|
|
296
331
|
logger.warning(_MSG_SITEMAP_NOT_FOUND_IN_ROBOTS, robots_url)
|
|
297
332
|
result = await self._try_default_sitemaps_async(base_url, result)
|
|
298
333
|
|
|
299
334
|
except Exception as e:
|
|
300
335
|
logger.error(_MSG_ROBOTS_READ_ERROR, base_url, e)
|
|
301
|
-
|
|
336
|
+
if not self._mark_if_origin_down_cached(robots_url, result):
|
|
337
|
+
result = await self._try_default_sitemaps_async(base_url, result)
|
|
302
338
|
|
|
303
339
|
result["urls"] = list(set(result["urls"]))
|
|
304
340
|
result["sitemap_indexes"] = list(set(result["sitemap_indexes"]))
|
|
@@ -316,10 +352,15 @@ class SitemapParser:
|
|
|
316
352
|
for sitemap_url in default_sitemaps:
|
|
317
353
|
try:
|
|
318
354
|
sitemap_data = await self.parse_sitemap_async(sitemap_url)
|
|
355
|
+
if sitemap_data.get("origin_down"):
|
|
356
|
+
result["origin_down"] = True
|
|
357
|
+
result["origin_down_reason"] = sitemap_data.get("origin_down_reason")
|
|
358
|
+
break
|
|
319
359
|
if sitemap_data["urls"] or sitemap_data["sitemap_indexes"]:
|
|
320
360
|
result["sitemap_urls"].append(sitemap_url)
|
|
321
361
|
result["urls"].extend(sitemap_data["urls"])
|
|
322
362
|
result["sitemap_indexes"].extend(sitemap_data["sitemap_indexes"])
|
|
363
|
+
result.setdefault("lastmod", {}).update(sitemap_data.get("lastmod", {}))
|
|
323
364
|
logger.info(_MSG_SITEMAP_FOUND_AT, sitemap_url)
|
|
324
365
|
break
|
|
325
366
|
except Exception as e:
|
|
@@ -346,10 +387,15 @@ class SitemapParser:
|
|
|
346
387
|
if cached is not None:
|
|
347
388
|
logger.debug("Sitemap cache hit: %s", sitemap_url)
|
|
348
389
|
return cached
|
|
390
|
+
if self._mark_if_origin_down_cached(sitemap_url, result):
|
|
391
|
+
return result
|
|
349
392
|
|
|
350
393
|
try:
|
|
351
394
|
fetched = await self._fetch(sitemap_url)
|
|
352
395
|
|
|
396
|
+
if self._check_origin(sitemap_url, fetched):
|
|
397
|
+
return self._with_origin_down(sitemap_url, result)
|
|
398
|
+
|
|
353
399
|
if fetched.status not in (200, 301, 302) or fetched.body is None:
|
|
354
400
|
logger.warning(
|
|
355
401
|
_MSG_SITEMAP_NOT_FOUND_STATUS,
|
|
@@ -439,8 +485,11 @@ class SitemapParser:
|
|
|
439
485
|
)
|
|
440
486
|
|
|
441
487
|
elif root.tag.endswith("urlset"):
|
|
442
|
-
|
|
443
|
-
result["urls"] = [self._normalize_url(u, base_url) for u in
|
|
488
|
+
entries = self._parse_urlset_entries(root)
|
|
489
|
+
result["urls"] = [self._normalize_url(u, base_url) for u, _ in entries]
|
|
490
|
+
result["lastmod"] = {
|
|
491
|
+
self._normalize_url(u, base_url): lastmod for u, lastmod in entries if lastmod
|
|
492
|
+
}
|
|
444
493
|
logger.info(_MSG_URLS_FOUND_IN_SITEMAP, len(result["urls"]), sitemap_url)
|
|
445
494
|
|
|
446
495
|
else:
|
|
@@ -475,7 +524,7 @@ class SitemapParser:
|
|
|
475
524
|
for line in content.splitlines():
|
|
476
525
|
line = line.strip()
|
|
477
526
|
if line.lower().startswith("sitemap:"):
|
|
478
|
-
url = line[8:]
|
|
527
|
+
url = self._clean_sitemap_value(line[8:])
|
|
479
528
|
if not url:
|
|
480
529
|
continue
|
|
481
530
|
normalized = self._normalize_url(url, base_url)
|
|
@@ -508,6 +557,86 @@ class SitemapParser:
|
|
|
508
557
|
return True
|
|
509
558
|
return False
|
|
510
559
|
|
|
560
|
+
@staticmethod
|
|
561
|
+
def _clean_sitemap_value(raw: str) -> str | None:
|
|
562
|
+
"""Витягує sitemap URL з (можливо брудного) значення директиви ``Sitemap:``.
|
|
563
|
+
|
|
564
|
+
Деякі robots.txt містять сміття перед адресою, напр.
|
|
565
|
+
``Sitemap: sitemap URL https://site.com/sitemap_index.xml``. Беремо
|
|
566
|
+
перший ``https?://\\S+`` із значення; якщо абсолютного URL немає, але
|
|
567
|
+
значення — відносний шлях (``/sitemap.xml``), повертаємо його для
|
|
568
|
+
подальшого ``urljoin``. Інакше — ``None`` (значення непридатне).
|
|
569
|
+
"""
|
|
570
|
+
raw = raw.strip()
|
|
571
|
+
if not raw:
|
|
572
|
+
return None
|
|
573
|
+
match = re.search(r"https?://\S+", raw)
|
|
574
|
+
if match:
|
|
575
|
+
return match.group(0)
|
|
576
|
+
if raw.startswith("/"):
|
|
577
|
+
return raw.split()[0]
|
|
578
|
+
return None
|
|
579
|
+
|
|
580
|
+
def origin_down_reason(self, url: str) -> str | None:
|
|
581
|
+
"""Причина ORIGIN_DOWN для хоста ``url`` (``cf_52x``/``timeout``) або ``None``."""
|
|
582
|
+
return self._down_hosts.get(urlparse(url).hostname or "")
|
|
583
|
+
|
|
584
|
+
def mark_origin_down(self, url: str, reason: str, detail: str | int | None = None) -> None:
|
|
585
|
+
"""Позначає origin хоста ``url`` лежачим: решта sitemap-фази хоста — без запитів."""
|
|
586
|
+
host = urlparse(url).hostname or ""
|
|
587
|
+
if not host or host in self._down_hosts:
|
|
588
|
+
return
|
|
589
|
+
self._down_hosts[host] = reason
|
|
590
|
+
logger.warning(
|
|
591
|
+
"ORIGIN_DOWN: %s (%s, %s on %s) — stopping sitemap phase for host without retries",
|
|
592
|
+
host,
|
|
593
|
+
reason,
|
|
594
|
+
detail,
|
|
595
|
+
url,
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
def _check_origin(self, url: str, fetched: _Fetched) -> bool:
|
|
599
|
+
"""``True`` і позначка хоста, якщо відповідь каже «origin лежить» (CF 52x або таймаут)."""
|
|
600
|
+
if self._is_origin_down(fetched.status):
|
|
601
|
+
self.mark_origin_down(url, ORIGIN_DOWN_CF_5XX, fetched.status)
|
|
602
|
+
return True
|
|
603
|
+
if self._is_timeout(fetched):
|
|
604
|
+
self.mark_origin_down(url, ORIGIN_DOWN_TIMEOUT, fetched.error)
|
|
605
|
+
return True
|
|
606
|
+
return False
|
|
607
|
+
|
|
608
|
+
def _with_origin_down(self, url: str, result: dict) -> dict:
|
|
609
|
+
result["origin_down"] = True
|
|
610
|
+
result["origin_down_reason"] = self.origin_down_reason(url)
|
|
611
|
+
return result
|
|
612
|
+
|
|
613
|
+
def _mark_if_origin_down_cached(self, url: str, result: dict) -> bool:
|
|
614
|
+
"""Хост уже позначено лежачим — віддаємо ``origin_down`` без запиту."""
|
|
615
|
+
if self.origin_down_reason(url) is None:
|
|
616
|
+
return False
|
|
617
|
+
logger.debug("ORIGIN_DOWN: skipping %s (host already down)", url)
|
|
618
|
+
self._with_origin_down(url, result)
|
|
619
|
+
return True
|
|
620
|
+
|
|
621
|
+
@staticmethod
|
|
622
|
+
def _is_timeout(fetched: _Fetched) -> bool:
|
|
623
|
+
"""Таймаут без відповіді сервера: статусу немає, у помилці — ознака таймауту."""
|
|
624
|
+
if fetched.status is not None or not fetched.error:
|
|
625
|
+
return False
|
|
626
|
+
error = fetched.error.lower()
|
|
627
|
+
return any(marker in error for marker in _TIMEOUT_ERROR_MARKERS)
|
|
628
|
+
|
|
629
|
+
@staticmethod
|
|
630
|
+
def _is_origin_down(status: int | None) -> bool:
|
|
631
|
+
"""``True`` для Cloudflare-origin помилок 520–527 (origin недоступний).
|
|
632
|
+
|
|
633
|
+
520 Web Server Returned an Unknown Error … 527 Railgun Error — проксі
|
|
634
|
+
Cloudflare не зміг дістатися origin. Ретраї/ескалація на robots/sitemap
|
|
635
|
+
безнадійні: origin лежить, а не блокує. Зупиняємо sitemap-фазу й віддаємо
|
|
636
|
+
сигнал ``ORIGIN_DOWN`` замість марних 100–180 с ретраїв.
|
|
637
|
+
"""
|
|
638
|
+
return status is not None and 520 <= status <= 527
|
|
639
|
+
|
|
511
640
|
def parse_from_robots(self, base_url: str) -> dict[str, list[str]]:
|
|
512
641
|
"""Sync-обгортка над ``parse_from_robots_async`` (``asyncio.run``).
|
|
513
642
|
|
|
@@ -555,18 +684,25 @@ class SitemapParser:
|
|
|
555
684
|
|
|
556
685
|
def _parse_urlset(self, root: Element) -> list[str]:
|
|
557
686
|
"""Повертає URL з urlset-документа (з namespace або без)."""
|
|
558
|
-
|
|
687
|
+
return [url for url, _ in self._parse_urlset_entries(root)]
|
|
559
688
|
|
|
560
|
-
|
|
689
|
+
def _parse_urlset_entries(self, root: Element) -> list[tuple[str, str | None]]:
|
|
690
|
+
"""``(loc, lastmod)`` кожного ``<url>`` urlset-документа (з namespace або без)."""
|
|
691
|
+
ns = self.SITEMAP_NS
|
|
692
|
+
url_elements = root.findall(".//sm:url", ns)
|
|
693
|
+
prefix = "sm:" if url_elements else ""
|
|
561
694
|
if not url_elements:
|
|
562
|
-
url_elements = root.findall(".//url
|
|
695
|
+
url_elements = root.findall(".//url")
|
|
563
696
|
|
|
697
|
+
entries = []
|
|
564
698
|
for url_elem in url_elements:
|
|
565
|
-
|
|
566
|
-
if
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
699
|
+
loc = url_elem.find(f"{prefix}loc", ns)
|
|
700
|
+
if loc is None or not loc.text or not loc.text.strip():
|
|
701
|
+
continue
|
|
702
|
+
lastmod = url_elem.find(f"{prefix}lastmod", ns)
|
|
703
|
+
value = lastmod.text.strip() if lastmod is not None and lastmod.text else None
|
|
704
|
+
entries.append((loc.text.strip(), value or None))
|
|
705
|
+
return entries
|
|
570
706
|
|
|
571
707
|
def parse_large_sitemap_streaming(self, filepath_or_content: str | bytes) -> list[str]:
|
|
572
708
|
"""Потоковий парсинг великих sitemap (пам'ять не залежить від розміру файлу).
|
|
@@ -209,6 +209,7 @@ class SitemapProcessor:
|
|
|
209
209
|
parent_sitemap_url: str,
|
|
210
210
|
depth: int = 2,
|
|
211
211
|
max_urls: int | None = None,
|
|
212
|
+
lastmod: dict[str, str] | None = None,
|
|
212
213
|
) -> list[SitemapNode]:
|
|
213
214
|
"""
|
|
214
215
|
Створює Node для кожного URL зі sitemap.
|
|
@@ -218,6 +219,7 @@ class SitemapProcessor:
|
|
|
218
219
|
parent_sitemap_url: URL батьківського sitemap
|
|
219
220
|
depth: Глибина у графі
|
|
220
221
|
max_urls: Максимальна кількість URL для обробки (None = всі)
|
|
222
|
+
lastmod: ``{url: <lastmod>}`` з того ж sitemap → ``SitemapNode.lastmod``
|
|
221
223
|
|
|
222
224
|
Returns:
|
|
223
225
|
Список створених SitemapNode
|
|
@@ -261,6 +263,7 @@ class SitemapProcessor:
|
|
|
261
263
|
url=url,
|
|
262
264
|
node_type="url",
|
|
263
265
|
parent_sitemap=parent_sitemap_url,
|
|
266
|
+
lastmod=(lastmod or {}).get(url),
|
|
264
267
|
depth=depth,
|
|
265
268
|
should_scan=False, # URL вузли не сканують (це кінцеві точки)
|
|
266
269
|
can_create_edges=False,
|