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