graph-crawler 5.3.4__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.4 → graph_crawler-5.3.5}/CHANGELOG.md +80 -0
- {graph_crawler-5.3.4/graph_crawler.egg-info → graph_crawler-5.3.5}/PKG-INFO +1 -1
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/_core.py +2 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/async_.py +2 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/client/client.py +3 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/sync.py +4 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/dto/mappers/node_mapper.py +1 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/crawl_coordinator.py +22 -3
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/filters/domain_filter.py +31 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/sitemap_parser.py +84 -12
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/sitemap_processor.py +3 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/sitemap_spider.py +1 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/spider.py +3 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/entities/graph.py +129 -7
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/entities/node.py +87 -11
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/entities/sitemap_node.py +3 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/configs.py +3 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/sqlite_eviction_storage.py +1 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/escalation.py +128 -19
- {graph_crawler-5.3.4 → graph_crawler-5.3.5/graph_crawler.egg-info}/PKG-INFO +1 -1
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/pyproject.toml +1 -1
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/LICENSE +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/MANIFEST.in +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/README.md +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/__version__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/ai/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/_distributed.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/_shared.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/_sitemap_distributed.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/cli.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/client/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/history_manager.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/project_init.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/server/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/server/crawl_monitor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/server/dashboard.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/server/rest_api.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/server/webhooks.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/server/websocket_manager.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/api/stats_collector.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/bootstrap.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/context/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/context/dependency_registry.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/context/graph_context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/context/merge_context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/dto/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/dto/edge_dto.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/dto/graph_dto.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/dto/mappers/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/dto/mappers/edge_mapper.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/dto/mappers/graph_mapper.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/dto/mappers/node_data_codec.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/dto/node_dto.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/dto/utils.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/application_container.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/driver_factory.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/base_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/csv_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/edge_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/excel_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/jsonl_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/node_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/parquet_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/exporters/sql_exporter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/finalize.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/low_memory_graph_saver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/resource_budget.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/scheduling/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/scheduling/rule_based_scheduler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/services/storage_factory.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/adaptive_throttler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/async_bridge.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/base_spider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/celery_batch_spider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/celery_spider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/checkpoint.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/dead_letter_queue.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/domain_rate_limiter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/filters/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/filters/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/filters/domain_patterns.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/filters/path_filter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/incremental_strategy.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/link_processor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/multiprocess_spider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/node_scanner.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/parsers/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/parsers/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/parsers/html_parser.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/progress_tracker.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/scheduler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/serialization_mixin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/spider_lifecycle.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/worker_pool.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/graph_export.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/graph_merge/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/graph_merge/graph_operations.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/graph_merge/merge_strategies.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/data/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/data/backends/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/data/backends/memory.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/data/backends/sqlite.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/data/interfaces.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/context/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/context/crawl_context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/entities/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/entities/edge.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/entities/edge_analysis.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/entities/graph_operations.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/entities/graph_statistics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/entities/merge_strategies.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/entities/redirect_mode.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/entities/registries.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/entities/strategies.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/crawl.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/edge.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/event_bus.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/events.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/fetch.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/idempotent_subscriber.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/legacy_type_map.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/llm.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/node.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/scheduler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/events/storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/adapter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/control_channel.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/crawl_context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/distributed_spider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/driver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/event_bus.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/exporters.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/filter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/merge_context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/node_interfaces.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/parser.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/plugin_manager.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/processor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/scanner.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/scheduler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/scheduling/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/scheduling/frontier.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/scheduling/scheduler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/spider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/stop_condition.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/storage/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/storage/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/storage/eviction.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/interfaces/storage/queue.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/adjacency_maintenance.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/cycle_detection.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/degree_ranking.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/depth_context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/display_title.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/duplicate_detection.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/duplicate_selector.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/duplicate_stats.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/edge_filter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/edge_statistics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/eviction_policy.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/graph_query_cascade.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/graph_statistics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/graph_traversal.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/language_versions.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/lsh_index.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/minimap_builder.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/node_content_resolver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/node_view_builder.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/pagination_guards.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/predicate_ops.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/schema_filter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/search_haystack.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/simhash_clustering.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/simhash_fallback.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/similarity_ranking.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/text_search.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/url_clustering.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/url_deduplication.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/url_normalizer.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/url_rule_matcher.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/services/url_tree_builder.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/domain_patterns.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/fetch_options.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/html_document.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/lifecycle.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/models.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/page_actions.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/redirect_hop.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/scheduling/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/scheduling/crawl_request.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/scheduling/retry_policy.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/settings.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/url_normalization.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/domain/value_objects/views.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/cache_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/chain.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/error_recovery_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/logging_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/proxy_health.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/proxy_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/proxy_models.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/proxy_selection.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/rate_limit_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/request_response_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/retry_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/robots_cache.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/robots_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/robots_validator.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/middleware/user_agent_middleware.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/builtin/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/builtin/stats_export_plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/crawl_engine/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/crawl_engine/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/crawl_engine/priority_provider.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/crawl_engine/smart_crawl.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/anti_bot_detection.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/anti_bot_playwright.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/anti_bot_scripts.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/anti_bot_stealth.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/captcha/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/captcha/detector.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/captcha/models.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/captcha/plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/engine/captcha/services.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/content_extractors/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/content_extractors/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/content_extractors/goose3_extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/content_extractors/newspaper_extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/content_extractors/plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/content_extractors/readability_extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/defaults.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/extractors/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/extractors/email_extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/extractors/phone_extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/extractors/price_extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/links.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/markdown_plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/metadata.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/smart_page_finder.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/constants.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/exceptions.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/extractor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/options.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/jsonld.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/microdata.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/opengraph.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/rdfa.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/parsers/twitter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/structured_data/result.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/text.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/node/url_normalizer.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/extensions/plugins/registry.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/adapters/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/adapters/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/adapters/selectolax_adapter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/celery_app.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/celery_batch.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/celery_job_task.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/celery_unified.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/config.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/easy_crawler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/messaging/worker_api.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/auto_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/eviction.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/graph_repository.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/json/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/json_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/lmdb_eviction_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/memory/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/memory_dedup_store.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/memory_frontier.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/memory_politeness_store.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/memory_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/mongodb_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/naming_strategy.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/postgresql_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/protocols/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/redis_dedup_store.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/redis_politeness_store.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/repository.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/sqlite/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/sqlite_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/file_job_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/file_queue_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/memory_job_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/memory_queue_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/postgresql_job_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/postgresql_queue_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/persistence/unified/unified_storage.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/config.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/driver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/driver_curl_cffi.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/autothrottle.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/autothrottle_adaptive.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/headers.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/http_cache.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/rate_limiter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/plugins/retry.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/async_http/stages.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/base_plugin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/compression/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/compression/strategy.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/connection_pool.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/core/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/core/base_async.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/core/body_decoder.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/core/mixins.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/core/redirect_utils.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/factory.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/adaptive_semaphore.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/batch_scheduler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/browser_factory.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/browser_profile.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/config.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/context.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/driver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/engine.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/fetch_metrics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/net/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/net/geoip.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/net/proxy_pool.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/net/rate_limiter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/page_actions.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/page_fetch.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/_network_record.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/captcha_detector.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/captcha_solver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/cookie_consent.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/form_filler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/human_bezier.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/network_log.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/ram_adaptive.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/screenshot.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/plugins/user_profile.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/pool_lifecycle.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/pooled_driver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/stages.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/playwright/stealth_hints.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/plugin_manager.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/protocols.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/undetected/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/infrastructure/transport/undetected/async_driver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/native/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/native/bloom_filter.pyx +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/native/html_parser.pyx +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/native/setup.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/native/url_utils.pyx +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/decorators/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/decorators/cache.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/decorators/log.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/decorators/retry.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/decorators/timing.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/error_tracing.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/extensions/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/extensions/memory_guard.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/extensions/state_manager.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/base_metrics_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/crawl_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/dlq_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/error_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/logging_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/metrics_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/node_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/plugin_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/storage_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/listeners/url_listener.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/metrics/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/metrics/memory_profiler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/metrics/metrics_collector.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/metrics/prometheus_metrics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/metrics_core.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/structured_logging.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/observability/tracing.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/optimizations/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/optimizations/generators.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/optimizations/memory_slots.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/optimizations/simhash_numba.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/py.typed +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/constants.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/dto/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/dto/edge_dto.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/dto/graph_dto.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/dto/node_dto.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/error_handling/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/error_handling/error_handler.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/exceptions.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/platform/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/platform/adaptive_resolver.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/platform/config.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/platform/detector.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/platform/diagnostics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/platform/metrics.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/security/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/security/url_sanitizer.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/security/url_validator.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/async_compat.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/bloom_filter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/captcha/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/captcha/base.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/captcha/manager.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/celery_config.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/celery_helpers.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/cgroup_limits.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/content_decoder.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/distributed_rate_limiter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/distributed_rate_limiter_backends.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/dns_cache.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/enterprise_ua_generator.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/event_publisher_mixin.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/fast_json.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/fingerprint.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/fingerprint_data.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/fingerprint_generators.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/fingerprint_profile.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/fingerprint_stealth.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/html_utils.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/id_generator.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/isolated_executor.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/memory_optimizer.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/rate_limiter.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/regex_validator.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/shadow_dom.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/text_utils.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/url_patterns.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/url_utils.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/user_agent_rotator.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/validation_helpers.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/visualization.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/shared/utils/visualization_core.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/tools/__init__.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/tools/db_cleanup.py +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler.egg-info/SOURCES.txt +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler.egg-info/dependency_links.txt +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler.egg-info/entry_points.txt +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler.egg-info/requires.txt +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler.egg-info/top_level.txt +0 -0
- {graph_crawler-5.3.4 → graph_crawler-5.3.5}/setup.cfg +0 -0
|
@@ -1,5 +1,85 @@
|
|
|
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
|
+
|
|
3
83
|
## 5.3.4 — 2026-10
|
|
4
84
|
|
|
5
85
|
### 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
|
|
@@ -267,6 +268,16 @@ class SitemapParser:
|
|
|
267
268
|
robots_url = urljoin(base_url, "/robots.txt")
|
|
268
269
|
fetched = await self._fetch(robots_url)
|
|
269
270
|
|
|
271
|
+
if self._is_origin_down(fetched.status):
|
|
272
|
+
logger.warning(
|
|
273
|
+
"ORIGIN_DOWN: %s returned %s (Cloudflare origin error) — "
|
|
274
|
+
"stopping sitemap phase without retries",
|
|
275
|
+
robots_url,
|
|
276
|
+
fetched.status,
|
|
277
|
+
)
|
|
278
|
+
result["origin_down"] = True
|
|
279
|
+
return result
|
|
280
|
+
|
|
270
281
|
if fetched.status != 200 or fetched.body is None:
|
|
271
282
|
logger.warning("robots.txt not found at %s", robots_url)
|
|
272
283
|
return await self._try_default_sitemaps_async(base_url, result)
|
|
@@ -275,7 +286,10 @@ class SitemapParser:
|
|
|
275
286
|
body = fetched.body
|
|
276
287
|
robots_content = body if isinstance(body, str) else body.decode("utf-8", "replace")
|
|
277
288
|
|
|
278
|
-
|
|
289
|
+
# Origin і база відносних шляхів — хост, що реально віддав robots.txt:
|
|
290
|
+
# після переїзду домену (kilo.health → kilo.co) його Sitemap: вказує на новий хост.
|
|
291
|
+
robots_base = fetched.final_url or base_url
|
|
292
|
+
sitemap_urls = self._parse_robots_txt(robots_content, robots_base)
|
|
279
293
|
|
|
280
294
|
if sitemap_urls:
|
|
281
295
|
result["sitemap_urls"] = sitemap_urls
|
|
@@ -290,8 +304,11 @@ class SitemapParser:
|
|
|
290
304
|
logger.warning("Sitemap parse error: %s", sitemap_data)
|
|
291
305
|
continue
|
|
292
306
|
if isinstance(sitemap_data, dict):
|
|
307
|
+
if sitemap_data.get("origin_down"):
|
|
308
|
+
result["origin_down"] = True
|
|
293
309
|
result["urls"].extend(sitemap_data.get("urls", []))
|
|
294
310
|
result["sitemap_indexes"].extend(sitemap_data.get("sitemap_indexes", []))
|
|
311
|
+
result.setdefault("lastmod", {}).update(sitemap_data.get("lastmod", {}))
|
|
295
312
|
else:
|
|
296
313
|
logger.warning(_MSG_SITEMAP_NOT_FOUND_IN_ROBOTS, robots_url)
|
|
297
314
|
result = await self._try_default_sitemaps_async(base_url, result)
|
|
@@ -316,10 +333,14 @@ class SitemapParser:
|
|
|
316
333
|
for sitemap_url in default_sitemaps:
|
|
317
334
|
try:
|
|
318
335
|
sitemap_data = await self.parse_sitemap_async(sitemap_url)
|
|
336
|
+
if sitemap_data.get("origin_down"):
|
|
337
|
+
result["origin_down"] = True
|
|
338
|
+
break
|
|
319
339
|
if sitemap_data["urls"] or sitemap_data["sitemap_indexes"]:
|
|
320
340
|
result["sitemap_urls"].append(sitemap_url)
|
|
321
341
|
result["urls"].extend(sitemap_data["urls"])
|
|
322
342
|
result["sitemap_indexes"].extend(sitemap_data["sitemap_indexes"])
|
|
343
|
+
result.setdefault("lastmod", {}).update(sitemap_data.get("lastmod", {}))
|
|
323
344
|
logger.info(_MSG_SITEMAP_FOUND_AT, sitemap_url)
|
|
324
345
|
break
|
|
325
346
|
except Exception as e:
|
|
@@ -350,6 +371,16 @@ class SitemapParser:
|
|
|
350
371
|
try:
|
|
351
372
|
fetched = await self._fetch(sitemap_url)
|
|
352
373
|
|
|
374
|
+
if self._is_origin_down(fetched.status):
|
|
375
|
+
logger.warning(
|
|
376
|
+
"ORIGIN_DOWN: sitemap %s returned %s (Cloudflare origin error) — "
|
|
377
|
+
"no retries",
|
|
378
|
+
sitemap_url,
|
|
379
|
+
fetched.status,
|
|
380
|
+
)
|
|
381
|
+
result["origin_down"] = True
|
|
382
|
+
return result
|
|
383
|
+
|
|
353
384
|
if fetched.status not in (200, 301, 302) or fetched.body is None:
|
|
354
385
|
logger.warning(
|
|
355
386
|
_MSG_SITEMAP_NOT_FOUND_STATUS,
|
|
@@ -439,8 +470,11 @@ class SitemapParser:
|
|
|
439
470
|
)
|
|
440
471
|
|
|
441
472
|
elif root.tag.endswith("urlset"):
|
|
442
|
-
|
|
443
|
-
result["urls"] = [self._normalize_url(u, base_url) for u in
|
|
473
|
+
entries = self._parse_urlset_entries(root)
|
|
474
|
+
result["urls"] = [self._normalize_url(u, base_url) for u, _ in entries]
|
|
475
|
+
result["lastmod"] = {
|
|
476
|
+
self._normalize_url(u, base_url): lastmod for u, lastmod in entries if lastmod
|
|
477
|
+
}
|
|
444
478
|
logger.info(_MSG_URLS_FOUND_IN_SITEMAP, len(result["urls"]), sitemap_url)
|
|
445
479
|
|
|
446
480
|
else:
|
|
@@ -475,7 +509,7 @@ class SitemapParser:
|
|
|
475
509
|
for line in content.splitlines():
|
|
476
510
|
line = line.strip()
|
|
477
511
|
if line.lower().startswith("sitemap:"):
|
|
478
|
-
url = line[8:]
|
|
512
|
+
url = self._clean_sitemap_value(line[8:])
|
|
479
513
|
if not url:
|
|
480
514
|
continue
|
|
481
515
|
normalized = self._normalize_url(url, base_url)
|
|
@@ -508,6 +542,37 @@ class SitemapParser:
|
|
|
508
542
|
return True
|
|
509
543
|
return False
|
|
510
544
|
|
|
545
|
+
@staticmethod
|
|
546
|
+
def _clean_sitemap_value(raw: str) -> str | None:
|
|
547
|
+
"""Витягує sitemap URL з (можливо брудного) значення директиви ``Sitemap:``.
|
|
548
|
+
|
|
549
|
+
Деякі robots.txt містять сміття перед адресою, напр.
|
|
550
|
+
``Sitemap: sitemap URL https://site.com/sitemap_index.xml``. Беремо
|
|
551
|
+
перший ``https?://\\S+`` із значення; якщо абсолютного URL немає, але
|
|
552
|
+
значення — відносний шлях (``/sitemap.xml``), повертаємо його для
|
|
553
|
+
подальшого ``urljoin``. Інакше — ``None`` (значення непридатне).
|
|
554
|
+
"""
|
|
555
|
+
raw = raw.strip()
|
|
556
|
+
if not raw:
|
|
557
|
+
return None
|
|
558
|
+
match = re.search(r"https?://\S+", raw)
|
|
559
|
+
if match:
|
|
560
|
+
return match.group(0)
|
|
561
|
+
if raw.startswith("/"):
|
|
562
|
+
return raw.split()[0]
|
|
563
|
+
return None
|
|
564
|
+
|
|
565
|
+
@staticmethod
|
|
566
|
+
def _is_origin_down(status: int | None) -> bool:
|
|
567
|
+
"""``True`` для Cloudflare-origin помилок 520–527 (origin недоступний).
|
|
568
|
+
|
|
569
|
+
520 Web Server Returned an Unknown Error … 527 Railgun Error — проксі
|
|
570
|
+
Cloudflare не зміг дістатися origin. Ретраї/ескалація на robots/sitemap
|
|
571
|
+
безнадійні: origin лежить, а не блокує. Зупиняємо sitemap-фазу й віддаємо
|
|
572
|
+
сигнал ``ORIGIN_DOWN`` замість марних 100–180 с ретраїв.
|
|
573
|
+
"""
|
|
574
|
+
return status is not None and 520 <= status <= 527
|
|
575
|
+
|
|
511
576
|
def parse_from_robots(self, base_url: str) -> dict[str, list[str]]:
|
|
512
577
|
"""Sync-обгортка над ``parse_from_robots_async`` (``asyncio.run``).
|
|
513
578
|
|
|
@@ -555,18 +620,25 @@ class SitemapParser:
|
|
|
555
620
|
|
|
556
621
|
def _parse_urlset(self, root: Element) -> list[str]:
|
|
557
622
|
"""Повертає URL з urlset-документа (з namespace або без)."""
|
|
558
|
-
|
|
623
|
+
return [url for url, _ in self._parse_urlset_entries(root)]
|
|
559
624
|
|
|
560
|
-
|
|
625
|
+
def _parse_urlset_entries(self, root: Element) -> list[tuple[str, str | None]]:
|
|
626
|
+
"""``(loc, lastmod)`` кожного ``<url>`` urlset-документа (з namespace або без)."""
|
|
627
|
+
ns = self.SITEMAP_NS
|
|
628
|
+
url_elements = root.findall(".//sm:url", ns)
|
|
629
|
+
prefix = "sm:" if url_elements else ""
|
|
561
630
|
if not url_elements:
|
|
562
|
-
url_elements = root.findall(".//url
|
|
631
|
+
url_elements = root.findall(".//url")
|
|
563
632
|
|
|
633
|
+
entries = []
|
|
564
634
|
for url_elem in url_elements:
|
|
565
|
-
|
|
566
|
-
if
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
635
|
+
loc = url_elem.find(f"{prefix}loc", ns)
|
|
636
|
+
if loc is None or not loc.text or not loc.text.strip():
|
|
637
|
+
continue
|
|
638
|
+
lastmod = url_elem.find(f"{prefix}lastmod", ns)
|
|
639
|
+
value = lastmod.text.strip() if lastmod is not None and lastmod.text else None
|
|
640
|
+
entries.append((loc.text.strip(), value or None))
|
|
641
|
+
return entries
|
|
570
642
|
|
|
571
643
|
def parse_large_sitemap_streaming(self, filepath_or_content: str | bytes) -> list[str]:
|
|
572
644
|
"""Потоковий парсинг великих 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,
|
|
@@ -560,6 +560,7 @@ class SitemapSpider(BaseSpider):
|
|
|
560
560
|
parent_sitemap_url=sitemap_url,
|
|
561
561
|
depth=depth + 1,
|
|
562
562
|
max_urls=budget,
|
|
563
|
+
lastmod=sitemap_data.get("lastmod"),
|
|
563
564
|
)
|
|
564
565
|
self.urls_extracted += len(url_nodes)
|
|
565
566
|
if self.max_urls and self.urls_extracted >= self.max_urls:
|
{graph_crawler-5.3.4 → graph_crawler-5.3.5}/graph_crawler/application/use_cases/crawling/spider.py
RENAMED
|
@@ -184,6 +184,9 @@ class GraphSpider(BaseSpider):
|
|
|
184
184
|
self.domain_filter = DomainFilter(domain_config, self.event_bus)
|
|
185
185
|
else:
|
|
186
186
|
self.domain_filter = domain_filter
|
|
187
|
+
alias_hook = getattr(self.config, "redirect_host_alias", None)
|
|
188
|
+
if alias_hook is not None and hasattr(self.domain_filter, "redirect_host_alias"):
|
|
189
|
+
self.domain_filter.redirect_host_alias = alias_hook
|
|
187
190
|
|
|
188
191
|
# DI: Path filter
|
|
189
192
|
if path_filter is None:
|
|
@@ -392,20 +392,95 @@ class Graph:
|
|
|
392
392
|
return self._backend.iter_nodes_sync() # type: ignore
|
|
393
393
|
return iter(self._nodes.values())
|
|
394
394
|
|
|
395
|
-
async def iter_nodes_async(
|
|
395
|
+
async def iter_nodes_async(
|
|
396
|
+
self, batch_size: int = 1000, *, include_evicted: bool = False
|
|
397
|
+
) -> AsyncIterator[Node]:
|
|
396
398
|
"""Streaming-ітератор по нодах. Без завантаження всього графа в RAM.
|
|
397
399
|
|
|
400
|
+
Args:
|
|
401
|
+
batch_size: розмір батчу для backend-ітерації.
|
|
402
|
+
include_evicted: якщо ``True`` і граф працює без backend у
|
|
403
|
+
LOW-MEMORY режимі — після RAM-нод доітеровуються й ноди,
|
|
404
|
+
витіснені на диск (eviction storage). Без цього прапорця
|
|
405
|
+
merge браузер-рескану бачив лише RAM і губив витіснені
|
|
406
|
+
seed-вакансії (P23). При ``_use_backend`` evicted-ноди вже
|
|
407
|
+
в backend, тож прапорець нічого не змінює.
|
|
408
|
+
|
|
398
409
|
Приклад:
|
|
399
|
-
async for node in graph.iter_nodes_async():
|
|
410
|
+
async for node in graph.iter_nodes_async(include_evicted=True):
|
|
400
411
|
await process(node)
|
|
401
412
|
"""
|
|
402
413
|
if self._use_backend and hasattr(self._backend, "iter_nodes"):
|
|
403
414
|
async for node in self._backend.iter_nodes(batch_size=batch_size): # type: ignore
|
|
404
415
|
yield node
|
|
405
|
-
|
|
406
|
-
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
seen_ids: set[str] = set()
|
|
419
|
+
for node in list(self._nodes.values()):
|
|
420
|
+
seen_ids.add(node.node_id)
|
|
421
|
+
yield node
|
|
422
|
+
|
|
423
|
+
if not include_evicted or not self._eviction_storage or not self._evicted_urls:
|
|
424
|
+
return
|
|
425
|
+
for url in list(self._evicted_urls):
|
|
426
|
+
try:
|
|
427
|
+
node_data = self._eviction_storage.load_node_sync(url)
|
|
428
|
+
except Exception as exc: # noqa: BLE001
|
|
429
|
+
logger.warning("iter_nodes_async: failed to load evicted node %s: %s", url, exc)
|
|
430
|
+
continue
|
|
431
|
+
node = self._materialize_evicted_node(node_data)
|
|
432
|
+
if node is not None and node.node_id not in seen_ids:
|
|
433
|
+
seen_ids.add(node.node_id)
|
|
407
434
|
yield node
|
|
408
435
|
|
|
436
|
+
def _materialize_evicted_node(self, node_data: Any) -> Node | None:
|
|
437
|
+
"""Будує :class:`Node` з вивантаженого payload БЕЗ повернення в RAM.
|
|
438
|
+
|
|
439
|
+
Дзеркалить реконструкцію `_load_node_from_disk` (підклас Node, кастомні
|
|
440
|
+
поля з `user_data['_custom_fields']`), але не мутує `_nodes`/`_evicted_urls`:
|
|
441
|
+
ітерація по витіснених нодах має лишати їх на диску (інакше merge
|
|
442
|
+
матеріалізував би весь граф у RAM — те саме LOW-MEMORY-переповнення).
|
|
443
|
+
"""
|
|
444
|
+
if node_data is None:
|
|
445
|
+
return None
|
|
446
|
+
if isinstance(node_data, Node):
|
|
447
|
+
return node_data
|
|
448
|
+
if not isinstance(node_data, dict):
|
|
449
|
+
return None
|
|
450
|
+
try:
|
|
451
|
+
user_data = node_data.get("user_data") or {}
|
|
452
|
+
if isinstance(user_data, dict) and "_node_class" in user_data:
|
|
453
|
+
user_data = {k: v for k, v in user_data.items() if k != "_node_class"}
|
|
454
|
+
raw_custom_fields = (
|
|
455
|
+
user_data.get("_custom_fields", {}) if isinstance(user_data, dict) else {}
|
|
456
|
+
)
|
|
457
|
+
from graph_crawler.application.dto.mappers.node_data_codec import NodeDataCodec
|
|
458
|
+
|
|
459
|
+
custom_fields = NodeDataCodec.decode(raw_custom_fields) if raw_custom_fields else {}
|
|
460
|
+
node_cls: type[Node] = getattr(self, "_node_class", None) or Node
|
|
461
|
+
node_kwargs = {
|
|
462
|
+
"url": node_data["url"],
|
|
463
|
+
"node_id": node_data["node_id"],
|
|
464
|
+
"depth": node_data["depth"],
|
|
465
|
+
"scanned": node_data["scanned"],
|
|
466
|
+
}
|
|
467
|
+
if custom_fields:
|
|
468
|
+
node_kwargs.update(
|
|
469
|
+
{k: v for k, v in custom_fields.items() if k in node_cls.model_fields}
|
|
470
|
+
)
|
|
471
|
+
node = node_cls(**node_kwargs)
|
|
472
|
+
for attr in ("response_status", "content_hash", "simhash", "priority"):
|
|
473
|
+
if node_data.get(attr):
|
|
474
|
+
setattr(node, attr, node_data[attr])
|
|
475
|
+
if node_data.get("metadata") is not None:
|
|
476
|
+
node.metadata = node_data["metadata"]
|
|
477
|
+
if user_data:
|
|
478
|
+
node.user_data = user_data
|
|
479
|
+
return node
|
|
480
|
+
except Exception as exc: # noqa: BLE001
|
|
481
|
+
logger.warning("iter_nodes_async: cannot build Node from evicted payload: %s", exc)
|
|
482
|
+
return None
|
|
483
|
+
|
|
409
484
|
def iter_edges(self) -> Iterator[Edge]:
|
|
410
485
|
"""Ітератор по всіх ребрах графу."""
|
|
411
486
|
if self._use_backend and hasattr(self._backend, "iter_edges_sync"):
|
|
@@ -1986,10 +2061,16 @@ class Graph:
|
|
|
1986
2061
|
from itertools import chain
|
|
1987
2062
|
|
|
1988
2063
|
protected = exclude_node_ids or ()
|
|
1989
|
-
# Lazy generators - не створюють проміжні
|
|
1990
|
-
|
|
2064
|
+
# Lazy generators - не створюють проміжні списки.
|
|
2065
|
+
# P23: закріплені (pinned) ноди не витісняються — це seed-вакансії
|
|
2066
|
+
# браузер-рескану, які в LOW-MEMORY режимі найстаріші й пропадали першими.
|
|
2067
|
+
scanned = (
|
|
2068
|
+
n for n in self.iter_nodes()
|
|
2069
|
+
if n.scanned and n.node_id not in protected and not getattr(n, "pinned", False)
|
|
2070
|
+
)
|
|
1991
2071
|
unscanned = (
|
|
1992
|
-
n for n in self.iter_nodes()
|
|
2072
|
+
n for n in self.iter_nodes()
|
|
2073
|
+
if not n.scanned and n.node_id not in protected and not getattr(n, "pinned", False)
|
|
1993
2074
|
)
|
|
1994
2075
|
|
|
1995
2076
|
# chain - lazy concatenation, list() матеріалізує тільки раз
|
|
@@ -2352,6 +2433,47 @@ class Graph:
|
|
|
2352
2433
|
"""
|
|
2353
2434
|
return len(self._evicted_urls)
|
|
2354
2435
|
|
|
2436
|
+
@property
|
|
2437
|
+
def eviction_stats(self) -> dict[str, Any]:
|
|
2438
|
+
"""Лічильники eviction самого графа (RAM-mutations), не backend-сховища.
|
|
2439
|
+
|
|
2440
|
+
P23: merge браузер-рескану має читати саме це (граф без backend сам
|
|
2441
|
+
витісняє в LMDB/SQLite, а `backend.get_stats()` цього не бачить і показує
|
|
2442
|
+
0). Копія, щоб споживач не мутував внутрішній стан.
|
|
2443
|
+
"""
|
|
2444
|
+
return dict(self._eviction_stats)
|
|
2445
|
+
|
|
2446
|
+
def pin_node(self, node_id_or_url: str) -> bool:
|
|
2447
|
+
"""Закріплює ноду (P23): eviction її більше не витісняє.
|
|
2448
|
+
|
|
2449
|
+
Приймає node_id або URL. Для seed-вакансій браузер-рескану це гарантує,
|
|
2450
|
+
що вони доживуть до merge навіть у LOW-MEMORY режимі.
|
|
2451
|
+
|
|
2452
|
+
Returns:
|
|
2453
|
+
True, якщо ноду знайдено в RAM і закріплено.
|
|
2454
|
+
"""
|
|
2455
|
+
node = self._nodes.get(node_id_or_url)
|
|
2456
|
+
if node is None:
|
|
2457
|
+
node = self._url_to_node.get(self._normalize_url(node_id_or_url))
|
|
2458
|
+
if node is None:
|
|
2459
|
+
return False
|
|
2460
|
+
node.pinned = True
|
|
2461
|
+
return True
|
|
2462
|
+
|
|
2463
|
+
def pin_seed_nodes(self) -> int:
|
|
2464
|
+
"""Закріплює всі seed-ноди (``depth == 0``) у RAM. Повертає їх кількість.
|
|
2465
|
+
|
|
2466
|
+
Seed-и браузер-рескану — це знайдені вакансії; вони найстаріші й у
|
|
2467
|
+
LOW-MEMORY режимі витісняються першими (P23). Викликається перед
|
|
2468
|
+
фазою, що може тригерити eviction.
|
|
2469
|
+
"""
|
|
2470
|
+
pinned = 0
|
|
2471
|
+
for node in self._nodes.values():
|
|
2472
|
+
if node.depth == 0 and not node.pinned:
|
|
2473
|
+
node.pinned = True
|
|
2474
|
+
pinned += 1
|
|
2475
|
+
return pinned
|
|
2476
|
+
|
|
2355
2477
|
def load_all_from_disk(self, *, include_edges: bool = True) -> int:
|
|
2356
2478
|
"""
|
|
2357
2479
|
Завантажує всі evicted ноди (і, за замовчуванням, ребра) назад в RAM.
|