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.
Files changed (481) hide show
  1. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/CHANGELOG.md +100 -0
  2. {graph_crawler-5.3.4/graph_crawler.egg-info → graph_crawler-5.3.6}/PKG-INFO +1 -1
  3. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/_core.py +2 -0
  4. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/async_.py +2 -0
  5. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/client/client.py +3 -0
  6. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/sync.py +4 -0
  7. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/mappers/node_mapper.py +1 -0
  8. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/crawl_coordinator.py +22 -3
  9. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/filters/domain_filter.py +31 -0
  10. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/sitemap_parser.py +150 -14
  11. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/sitemap_processor.py +3 -0
  12. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/sitemap_spider.py +48 -3
  13. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/spider.py +3 -0
  14. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/graph.py +129 -7
  15. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/node.py +87 -11
  16. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/sitemap_node.py +3 -0
  17. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/configs.py +3 -0
  18. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/sqlite_eviction_storage.py +1 -0
  19. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/escalation.py +128 -19
  20. {graph_crawler-5.3.4 → graph_crawler-5.3.6/graph_crawler.egg-info}/PKG-INFO +1 -1
  21. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/pyproject.toml +1 -1
  22. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/LICENSE +0 -0
  23. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/MANIFEST.in +0 -0
  24. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/README.md +0 -0
  25. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/__init__.py +0 -0
  26. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/__version__.py +0 -0
  27. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/ai/__init__.py +0 -0
  28. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/__init__.py +0 -0
  29. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/_distributed.py +0 -0
  30. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/_shared.py +0 -0
  31. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/_sitemap_distributed.py +0 -0
  32. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/cli.py +0 -0
  33. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/client/__init__.py +0 -0
  34. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/history_manager.py +0 -0
  35. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/project_init.py +0 -0
  36. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/server/__init__.py +0 -0
  37. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/server/crawl_monitor.py +0 -0
  38. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/server/dashboard.py +0 -0
  39. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/server/rest_api.py +0 -0
  40. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/server/webhooks.py +0 -0
  41. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/server/websocket_manager.py +0 -0
  42. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/api/stats_collector.py +0 -0
  43. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/__init__.py +0 -0
  44. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/bootstrap.py +0 -0
  45. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/context/__init__.py +0 -0
  46. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/context/dependency_registry.py +0 -0
  47. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/context/graph_context.py +0 -0
  48. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/context/merge_context.py +0 -0
  49. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/__init__.py +0 -0
  50. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/edge_dto.py +0 -0
  51. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/graph_dto.py +0 -0
  52. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/mappers/__init__.py +0 -0
  53. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/mappers/edge_mapper.py +0 -0
  54. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/mappers/graph_mapper.py +0 -0
  55. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/mappers/node_data_codec.py +0 -0
  56. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/node_dto.py +0 -0
  57. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/dto/utils.py +0 -0
  58. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/__init__.py +0 -0
  59. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/application_container.py +0 -0
  60. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/driver_factory.py +0 -0
  61. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/__init__.py +0 -0
  62. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/base_exporter.py +0 -0
  63. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/csv_exporter.py +0 -0
  64. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/edge_exporter.py +0 -0
  65. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/excel_exporter.py +0 -0
  66. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/jsonl_exporter.py +0 -0
  67. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/node_exporter.py +0 -0
  68. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/parquet_exporter.py +0 -0
  69. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/exporters/sql_exporter.py +0 -0
  70. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/finalize.py +0 -0
  71. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/low_memory_graph_saver.py +0 -0
  72. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/resource_budget.py +0 -0
  73. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/scheduling/__init__.py +0 -0
  74. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/scheduling/rule_based_scheduler.py +0 -0
  75. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/services/storage_factory.py +0 -0
  76. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/__init__.py +0 -0
  77. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/__init__.py +0 -0
  78. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/adaptive_throttler.py +0 -0
  79. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/async_bridge.py +0 -0
  80. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/base_spider.py +0 -0
  81. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/celery_batch_spider.py +0 -0
  82. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/celery_spider.py +0 -0
  83. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/checkpoint.py +0 -0
  84. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/dead_letter_queue.py +0 -0
  85. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/domain_rate_limiter.py +0 -0
  86. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/filters/__init__.py +0 -0
  87. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/filters/base.py +0 -0
  88. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/filters/domain_patterns.py +0 -0
  89. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/filters/path_filter.py +0 -0
  90. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/incremental_strategy.py +0 -0
  91. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/link_processor.py +0 -0
  92. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/multiprocess_spider.py +0 -0
  93. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/node_scanner.py +0 -0
  94. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/parsers/__init__.py +0 -0
  95. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/parsers/base.py +0 -0
  96. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/parsers/html_parser.py +0 -0
  97. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/progress_tracker.py +0 -0
  98. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/scheduler.py +0 -0
  99. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/serialization_mixin.py +0 -0
  100. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/spider_lifecycle.py +0 -0
  101. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/crawling/worker_pool.py +0 -0
  102. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/graph_export.py +0 -0
  103. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/graph_merge/__init__.py +0 -0
  104. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/graph_merge/graph_operations.py +0 -0
  105. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/application/use_cases/graph_merge/merge_strategies.py +0 -0
  106. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/data/__init__.py +0 -0
  107. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/data/backends/__init__.py +0 -0
  108. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/data/backends/memory.py +0 -0
  109. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/data/backends/sqlite.py +0 -0
  110. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/data/interfaces.py +0 -0
  111. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/__init__.py +0 -0
  112. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/context/__init__.py +0 -0
  113. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/context/crawl_context.py +0 -0
  114. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/__init__.py +0 -0
  115. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/edge.py +0 -0
  116. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/edge_analysis.py +0 -0
  117. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/graph_operations.py +0 -0
  118. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/graph_statistics.py +0 -0
  119. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/merge_strategies.py +0 -0
  120. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/redirect_mode.py +0 -0
  121. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/registries.py +0 -0
  122. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/entities/strategies.py +0 -0
  123. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/__init__.py +0 -0
  124. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/base.py +0 -0
  125. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/crawl.py +0 -0
  126. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/edge.py +0 -0
  127. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/event_bus.py +0 -0
  128. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/events.py +0 -0
  129. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/fetch.py +0 -0
  130. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/idempotent_subscriber.py +0 -0
  131. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/legacy_type_map.py +0 -0
  132. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/llm.py +0 -0
  133. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/node.py +0 -0
  134. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/plugin.py +0 -0
  135. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/scheduler.py +0 -0
  136. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/events/storage.py +0 -0
  137. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/__init__.py +0 -0
  138. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/adapter.py +0 -0
  139. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/control_channel.py +0 -0
  140. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/crawl_context.py +0 -0
  141. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/distributed_spider.py +0 -0
  142. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/driver.py +0 -0
  143. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/event_bus.py +0 -0
  144. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/exporters.py +0 -0
  145. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/filter.py +0 -0
  146. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/merge_context.py +0 -0
  147. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/node_interfaces.py +0 -0
  148. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/parser.py +0 -0
  149. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/plugin_manager.py +0 -0
  150. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/processor.py +0 -0
  151. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/scanner.py +0 -0
  152. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/scheduler.py +0 -0
  153. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/scheduling/__init__.py +0 -0
  154. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/scheduling/frontier.py +0 -0
  155. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/scheduling/scheduler.py +0 -0
  156. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/spider.py +0 -0
  157. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/stop_condition.py +0 -0
  158. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/storage/__init__.py +0 -0
  159. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/storage/base.py +0 -0
  160. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/storage/eviction.py +0 -0
  161. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/interfaces/storage/queue.py +0 -0
  162. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/__init__.py +0 -0
  163. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/adjacency_maintenance.py +0 -0
  164. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/cycle_detection.py +0 -0
  165. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/degree_ranking.py +0 -0
  166. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/depth_context.py +0 -0
  167. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/display_title.py +0 -0
  168. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/duplicate_detection.py +0 -0
  169. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/duplicate_selector.py +0 -0
  170. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/duplicate_stats.py +0 -0
  171. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/edge_filter.py +0 -0
  172. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/edge_statistics.py +0 -0
  173. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/eviction_policy.py +0 -0
  174. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/graph_query_cascade.py +0 -0
  175. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/graph_statistics.py +0 -0
  176. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/graph_traversal.py +0 -0
  177. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/language_versions.py +0 -0
  178. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/lsh_index.py +0 -0
  179. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/minimap_builder.py +0 -0
  180. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/node_content_resolver.py +0 -0
  181. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/node_view_builder.py +0 -0
  182. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/pagination_guards.py +0 -0
  183. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/predicate_ops.py +0 -0
  184. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/schema_filter.py +0 -0
  185. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/search_haystack.py +0 -0
  186. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/simhash_clustering.py +0 -0
  187. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/simhash_fallback.py +0 -0
  188. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/similarity_ranking.py +0 -0
  189. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/text_search.py +0 -0
  190. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/url_clustering.py +0 -0
  191. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/url_deduplication.py +0 -0
  192. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/url_normalizer.py +0 -0
  193. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/url_rule_matcher.py +0 -0
  194. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/services/url_tree_builder.py +0 -0
  195. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/__init__.py +0 -0
  196. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/domain_patterns.py +0 -0
  197. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/fetch_options.py +0 -0
  198. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/html_document.py +0 -0
  199. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/lifecycle.py +0 -0
  200. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/models.py +0 -0
  201. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/page_actions.py +0 -0
  202. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/redirect_hop.py +0 -0
  203. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/scheduling/__init__.py +0 -0
  204. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/scheduling/crawl_request.py +0 -0
  205. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/scheduling/retry_policy.py +0 -0
  206. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/settings.py +0 -0
  207. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/url_normalization.py +0 -0
  208. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/domain/value_objects/views.py +0 -0
  209. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/__init__.py +0 -0
  210. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/__init__.py +0 -0
  211. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/base.py +0 -0
  212. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/cache_middleware.py +0 -0
  213. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/chain.py +0 -0
  214. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/error_recovery_middleware.py +0 -0
  215. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/logging_middleware.py +0 -0
  216. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/proxy_health.py +0 -0
  217. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/proxy_middleware.py +0 -0
  218. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/proxy_models.py +0 -0
  219. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/proxy_selection.py +0 -0
  220. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/rate_limit_middleware.py +0 -0
  221. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/request_response_middleware.py +0 -0
  222. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/retry_middleware.py +0 -0
  223. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/robots_cache.py +0 -0
  224. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/robots_middleware.py +0 -0
  225. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/robots_validator.py +0 -0
  226. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/middleware/user_agent_middleware.py +0 -0
  227. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/__init__.py +0 -0
  228. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/builtin/__init__.py +0 -0
  229. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/builtin/stats_export_plugin.py +0 -0
  230. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/crawl_engine/__init__.py +0 -0
  231. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/crawl_engine/base.py +0 -0
  232. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/crawl_engine/priority_provider.py +0 -0
  233. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/crawl_engine/smart_crawl.py +0 -0
  234. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/__init__.py +0 -0
  235. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/anti_bot_detection.py +0 -0
  236. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/anti_bot_playwright.py +0 -0
  237. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/anti_bot_scripts.py +0 -0
  238. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/anti_bot_stealth.py +0 -0
  239. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/base.py +0 -0
  240. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/captcha/__init__.py +0 -0
  241. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/captcha/detector.py +0 -0
  242. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/captcha/models.py +0 -0
  243. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/captcha/plugin.py +0 -0
  244. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/engine/captcha/services.py +0 -0
  245. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/__init__.py +0 -0
  246. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/base.py +0 -0
  247. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/content_extractors/__init__.py +0 -0
  248. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/content_extractors/base.py +0 -0
  249. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/content_extractors/goose3_extractor.py +0 -0
  250. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/content_extractors/newspaper_extractor.py +0 -0
  251. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/content_extractors/plugin.py +0 -0
  252. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/content_extractors/readability_extractor.py +0 -0
  253. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/defaults.py +0 -0
  254. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/extractors/__init__.py +0 -0
  255. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/extractors/email_extractor.py +0 -0
  256. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/extractors/phone_extractor.py +0 -0
  257. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/extractors/price_extractor.py +0 -0
  258. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/links.py +0 -0
  259. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/markdown_plugin.py +0 -0
  260. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/metadata.py +0 -0
  261. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/smart_page_finder.py +0 -0
  262. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/__init__.py +0 -0
  263. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/constants.py +0 -0
  264. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/exceptions.py +0 -0
  265. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/extractor.py +0 -0
  266. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/options.py +0 -0
  267. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/__init__.py +0 -0
  268. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/base.py +0 -0
  269. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/jsonld.py +0 -0
  270. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/microdata.py +0 -0
  271. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/opengraph.py +0 -0
  272. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/rdfa.py +0 -0
  273. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/parsers/twitter.py +0 -0
  274. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/plugin.py +0 -0
  275. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/structured_data/result.py +0 -0
  276. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/text.py +0 -0
  277. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/node/url_normalizer.py +0 -0
  278. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/extensions/plugins/registry.py +0 -0
  279. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/__init__.py +0 -0
  280. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/adapters/__init__.py +0 -0
  281. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/adapters/base.py +0 -0
  282. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/adapters/selectolax_adapter.py +0 -0
  283. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/__init__.py +0 -0
  284. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/celery_app.py +0 -0
  285. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/celery_batch.py +0 -0
  286. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/celery_job_task.py +0 -0
  287. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/celery_unified.py +0 -0
  288. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/config.py +0 -0
  289. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/easy_crawler.py +0 -0
  290. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/messaging/worker_api.py +0 -0
  291. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/__init__.py +0 -0
  292. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/auto_storage.py +0 -0
  293. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/base.py +0 -0
  294. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/eviction.py +0 -0
  295. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/graph_repository.py +0 -0
  296. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/json/__init__.py +0 -0
  297. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/json_storage.py +0 -0
  298. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/lmdb_eviction_storage.py +0 -0
  299. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/memory/__init__.py +0 -0
  300. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/memory_dedup_store.py +0 -0
  301. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/memory_frontier.py +0 -0
  302. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/memory_politeness_store.py +0 -0
  303. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/memory_storage.py +0 -0
  304. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/mongodb_storage.py +0 -0
  305. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/naming_strategy.py +0 -0
  306. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/postgresql_storage.py +0 -0
  307. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/protocols/__init__.py +0 -0
  308. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/redis_dedup_store.py +0 -0
  309. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/redis_politeness_store.py +0 -0
  310. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/repository.py +0 -0
  311. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/sqlite/__init__.py +0 -0
  312. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/sqlite_storage.py +0 -0
  313. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/__init__.py +0 -0
  314. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/file_job_storage.py +0 -0
  315. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/file_queue_storage.py +0 -0
  316. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/memory_job_storage.py +0 -0
  317. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/memory_queue_storage.py +0 -0
  318. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/postgresql_job_storage.py +0 -0
  319. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/postgresql_queue_storage.py +0 -0
  320. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/persistence/unified/unified_storage.py +0 -0
  321. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/__init__.py +0 -0
  322. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/__init__.py +0 -0
  323. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/config.py +0 -0
  324. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/context.py +0 -0
  325. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/driver.py +0 -0
  326. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/driver_curl_cffi.py +0 -0
  327. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/__init__.py +0 -0
  328. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/autothrottle.py +0 -0
  329. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/autothrottle_adaptive.py +0 -0
  330. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/headers.py +0 -0
  331. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/http_cache.py +0 -0
  332. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/rate_limiter.py +0 -0
  333. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/plugins/retry.py +0 -0
  334. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/async_http/stages.py +0 -0
  335. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/base.py +0 -0
  336. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/base_plugin.py +0 -0
  337. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/compression/__init__.py +0 -0
  338. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/compression/strategy.py +0 -0
  339. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/connection_pool.py +0 -0
  340. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/context.py +0 -0
  341. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/core/__init__.py +0 -0
  342. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/core/base_async.py +0 -0
  343. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/core/body_decoder.py +0 -0
  344. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/core/mixins.py +0 -0
  345. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/core/redirect_utils.py +0 -0
  346. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/factory.py +0 -0
  347. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/__init__.py +0 -0
  348. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/adaptive_semaphore.py +0 -0
  349. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/batch_scheduler.py +0 -0
  350. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/browser_factory.py +0 -0
  351. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/browser_profile.py +0 -0
  352. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/config.py +0 -0
  353. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/context.py +0 -0
  354. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/driver.py +0 -0
  355. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/engine.py +0 -0
  356. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/fetch_metrics.py +0 -0
  357. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/net/__init__.py +0 -0
  358. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/net/geoip.py +0 -0
  359. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/net/proxy_pool.py +0 -0
  360. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/net/rate_limiter.py +0 -0
  361. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/page_actions.py +0 -0
  362. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/page_fetch.py +0 -0
  363. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/__init__.py +0 -0
  364. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/_network_record.py +0 -0
  365. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/captcha_detector.py +0 -0
  366. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/captcha_solver.py +0 -0
  367. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/cookie_consent.py +0 -0
  368. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/form_filler.py +0 -0
  369. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/human_bezier.py +0 -0
  370. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/network_log.py +0 -0
  371. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/ram_adaptive.py +0 -0
  372. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/screenshot.py +0 -0
  373. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/plugins/user_profile.py +0 -0
  374. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/pool_lifecycle.py +0 -0
  375. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/pooled_driver.py +0 -0
  376. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/stages.py +0 -0
  377. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/playwright/stealth_hints.py +0 -0
  378. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/plugin_manager.py +0 -0
  379. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/protocols.py +0 -0
  380. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/undetected/__init__.py +0 -0
  381. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/infrastructure/transport/undetected/async_driver.py +0 -0
  382. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/native/__init__.py +0 -0
  383. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/native/bloom_filter.pyx +0 -0
  384. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/native/html_parser.pyx +0 -0
  385. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/native/setup.py +0 -0
  386. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/native/url_utils.pyx +0 -0
  387. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/__init__.py +0 -0
  388. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/decorators/__init__.py +0 -0
  389. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/decorators/cache.py +0 -0
  390. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/decorators/log.py +0 -0
  391. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/decorators/retry.py +0 -0
  392. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/decorators/timing.py +0 -0
  393. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/error_tracing.py +0 -0
  394. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/extensions/__init__.py +0 -0
  395. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/extensions/memory_guard.py +0 -0
  396. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/extensions/state_manager.py +0 -0
  397. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/__init__.py +0 -0
  398. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/base.py +0 -0
  399. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/base_metrics_listener.py +0 -0
  400. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/crawl_listener.py +0 -0
  401. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/dlq_listener.py +0 -0
  402. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/error_listener.py +0 -0
  403. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/logging_listener.py +0 -0
  404. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/metrics_listener.py +0 -0
  405. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/node_listener.py +0 -0
  406. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/plugin_listener.py +0 -0
  407. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/storage_listener.py +0 -0
  408. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/listeners/url_listener.py +0 -0
  409. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/metrics/__init__.py +0 -0
  410. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/metrics/memory_profiler.py +0 -0
  411. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/metrics/metrics_collector.py +0 -0
  412. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/metrics/prometheus_metrics.py +0 -0
  413. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/metrics_core.py +0 -0
  414. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/structured_logging.py +0 -0
  415. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/observability/tracing.py +0 -0
  416. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/optimizations/__init__.py +0 -0
  417. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/optimizations/generators.py +0 -0
  418. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/optimizations/memory_slots.py +0 -0
  419. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/optimizations/simhash_numba.py +0 -0
  420. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/py.typed +0 -0
  421. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/__init__.py +0 -0
  422. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/constants.py +0 -0
  423. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/dto/__init__.py +0 -0
  424. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/dto/edge_dto.py +0 -0
  425. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/dto/graph_dto.py +0 -0
  426. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/dto/node_dto.py +0 -0
  427. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/error_handling/__init__.py +0 -0
  428. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/error_handling/error_handler.py +0 -0
  429. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/exceptions.py +0 -0
  430. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/platform/__init__.py +0 -0
  431. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/platform/adaptive_resolver.py +0 -0
  432. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/platform/config.py +0 -0
  433. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/platform/detector.py +0 -0
  434. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/platform/diagnostics.py +0 -0
  435. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/platform/metrics.py +0 -0
  436. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/security/__init__.py +0 -0
  437. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/security/url_sanitizer.py +0 -0
  438. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/security/url_validator.py +0 -0
  439. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/__init__.py +0 -0
  440. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/async_compat.py +0 -0
  441. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/bloom_filter.py +0 -0
  442. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/captcha/__init__.py +0 -0
  443. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/captcha/base.py +0 -0
  444. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/captcha/manager.py +0 -0
  445. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/celery_config.py +0 -0
  446. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/celery_helpers.py +0 -0
  447. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/cgroup_limits.py +0 -0
  448. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/content_decoder.py +0 -0
  449. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/distributed_rate_limiter.py +0 -0
  450. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/distributed_rate_limiter_backends.py +0 -0
  451. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/dns_cache.py +0 -0
  452. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/enterprise_ua_generator.py +0 -0
  453. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/event_publisher_mixin.py +0 -0
  454. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/fast_json.py +0 -0
  455. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/fingerprint.py +0 -0
  456. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/fingerprint_data.py +0 -0
  457. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/fingerprint_generators.py +0 -0
  458. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/fingerprint_profile.py +0 -0
  459. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/fingerprint_stealth.py +0 -0
  460. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/html_utils.py +0 -0
  461. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/id_generator.py +0 -0
  462. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/isolated_executor.py +0 -0
  463. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/memory_optimizer.py +0 -0
  464. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/rate_limiter.py +0 -0
  465. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/regex_validator.py +0 -0
  466. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/shadow_dom.py +0 -0
  467. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/text_utils.py +0 -0
  468. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/url_patterns.py +0 -0
  469. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/url_utils.py +0 -0
  470. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/user_agent_rotator.py +0 -0
  471. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/validation_helpers.py +0 -0
  472. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/visualization.py +0 -0
  473. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/shared/utils/visualization_core.py +0 -0
  474. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/tools/__init__.py +0 -0
  475. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler/tools/db_cleanup.py +0 -0
  476. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler.egg-info/SOURCES.txt +0 -0
  477. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler.egg-info/dependency_links.txt +0 -0
  478. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler.egg-info/entry_points.txt +0 -0
  479. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler.egg-info/requires.txt +0 -0
  480. {graph_crawler-5.3.4 → graph_crawler-5.3.6}/graph_crawler.egg-info/top_level.txt +0 -0
  481. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: graph-crawler
3
- Version: 5.3.4
3
+ Version: 5.3.6
4
4
  Summary: Web crawler that builds a graph of a site. HTTP → TLS → browser escalation with pluggable browser engines.
5
5
  Author: 0-EternalJunior-0
6
6
  Maintainer: 0-EternalJunior-0
@@ -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,
@@ -59,6 +59,7 @@ class NodeMapper:
59
59
  "processed_at",
60
60
  "should_scan",
61
61
  "can_create_edges",
62
+ "pinned",
62
63
  "created_at",
63
64
  "metadata",
64
65
  "user_data",
@@ -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
- if domain_filter is not None and not domain_filter.is_allowed(
1009
- final_url, source_url=original_node.url
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
- sitemap_urls = self._parse_robots_txt(robots_content, base_url)
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
- result = await self._try_default_sitemaps_async(base_url, result)
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
- raw_urls = self._parse_urlset(root)
443
- result["urls"] = [self._normalize_url(u, base_url) for u in raw_urls]
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:].strip()
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
- urls = []
687
+ return [url for url, _ in self._parse_urlset_entries(root)]
559
688
 
560
- url_elements = root.findall(".//sm:url/sm:loc", self.SITEMAP_NS)
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/loc")
695
+ url_elements = root.findall(".//url")
563
696
 
697
+ entries = []
564
698
  for url_elem in url_elements:
565
- url = url_elem.text
566
- if url:
567
- urls.append(url.strip())
568
-
569
- return urls
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,