s3dgraphy 1.6.0.dev4__tar.gz → 1.6.0.dev6__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 (167) hide show
  1. {s3dgraphy-1.6.0.dev4/src/s3dgraphy.egg-info → s3dgraphy-1.6.0.dev6}/PKG-INFO +7 -3
  2. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/pyproject.toml +17 -3
  3. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/em_visual_rules.json +7 -6
  4. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/__init__.py +1 -1
  5. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/node_registry.py +26 -39
  6. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/rdf_exporter.py +101 -19
  7. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/importer/pyarchinit_importer.py +143 -17
  8. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/__init__.py +142 -0
  9. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/_db_handle.py +172 -0
  10. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/_legacy_paradata_svgs.py +264 -0
  11. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/_workspace.py +100 -0
  12. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/conflict_resolver.py +27 -0
  13. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/edge_registry.py +219 -0
  14. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/graph_ingestor.py +1528 -0
  15. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/graph_projector.py +1345 -0
  16. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/graphml_writer.py +2216 -0
  17. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/group_projector.py +214 -0
  18. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/group_store.py +436 -0
  19. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/ingest_result.py +64 -0
  20. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/paradata_store.py +952 -0
  21. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/pyarchinit_pg_importer.py +152 -0
  22. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/uuid7.py +85 -0
  23. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/vocab_provider_core.py +219 -0
  24. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/vocab_types.py +75 -0
  25. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/yed_classifier.py +222 -0
  26. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/yed_detector.py +69 -0
  27. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/yed_group_walker.py +194 -0
  28. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/yed_import_pipeline.py +1519 -0
  29. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/yed_rapporti_policy.py +443 -0
  30. s3dgraphy-1.6.0.dev6/src/s3dgraphy/sync/yed_table_parser.py +187 -0
  31. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6/src/s3dgraphy.egg-info}/PKG-INFO +7 -3
  32. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy.egg-info/SOURCES.txt +25 -1
  33. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy.egg-info/requires.txt +8 -2
  34. s3dgraphy-1.6.0.dev6/tests/test_pg_importer.py +307 -0
  35. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/LICENSE +0 -0
  36. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/MANIFEST.in +0 -0
  37. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/README.md +0 -0
  38. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/setup.cfg +0 -0
  39. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/em.ttl +0 -0
  40. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/em_document_types.json +0 -0
  41. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/em_extractor_types.json +0 -0
  42. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/em_palette_icons.json +0 -0
  43. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/em_qualia_types.json +0 -0
  44. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/em_qualia_types_additions.json +0 -0
  45. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/hdto_extension.ttl +0 -0
  46. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/new_qualia_template.json +0 -0
  47. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/outdated_s3dgraphy.ttl +0 -0
  48. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/outdated_s3dgraphy_nomapping.ttl +0 -0
  49. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/s3Dgraphy_connections_datamodel.json +0 -0
  50. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/s3Dgraphy_connections_datamodel.json.v155.bak +0 -0
  51. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/s3Dgraphy_node_datamodel.json +0 -0
  52. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/RSF.png +0 -0
  53. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/RSF.svg +0 -0
  54. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/SF.png +0 -0
  55. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/TSU.png +0 -0
  56. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/US.png +0 -0
  57. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/USD.png +0 -0
  58. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/USVn.png +0 -0
  59. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/USVs.png +0 -0
  60. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/VSF.png +0 -0
  61. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/combiner.png +0 -0
  62. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/continuity.png +0 -0
  63. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/document.png +0 -0
  64. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/extractor.png +0 -0
  65. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/property.png +0 -0
  66. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/serSU.png +0 -0
  67. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/serUSD.png +0 -0
  68. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/2D/serUSV.png +0 -0
  69. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/RSF.glb +0 -0
  70. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/SF.glb +0 -0
  71. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/TSU.glb +0 -0
  72. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/US.glb +0 -0
  73. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/USD.glb +0 -0
  74. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/USVn.glb +0 -0
  75. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/USVs.glb +0 -0
  76. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/VSF.glb +0 -0
  77. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/combiner.glb +0 -0
  78. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/continuity.glb +0 -0
  79. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/document.glb +0 -0
  80. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/extractor.glb +0 -0
  81. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/property.glb +0 -0
  82. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/serSU.glb +0 -0
  83. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/serUSD.glb +0 -0
  84. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/JSON_config/src/3D/serUSV.glb +0 -0
  85. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/classification.py +0 -0
  86. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/data/StratiMiner_Extraction_Prompt.md +0 -0
  87. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/diagnostics.py +0 -0
  88. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/edges/__init__.py +0 -0
  89. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/edges/connections_loader.py +0 -0
  90. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/edges/edge.py +0 -0
  91. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/__init__.py +0 -0
  92. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/__init__.py +0 -0
  93. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/canvas_generator.py +0 -0
  94. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/edge_generator.py +0 -0
  95. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/epoch_generator.py +0 -0
  96. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/graphml_exporter.py +0 -0
  97. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/graphml_patcher.py +0 -0
  98. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/group_node_generator.py +0 -0
  99. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/node_generator.py +0 -0
  100. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/palette_resources.py +0 -0
  101. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/paradata_generator.py +0 -0
  102. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/paradata_image_generator.py +0 -0
  103. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/paradata_node_generators.py +0 -0
  104. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/table_node_generator.py +0 -0
  105. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/graphml/utils.py +0 -0
  106. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/json_exporter.py +0 -0
  107. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/exporter/unified_xlsx_exporter.py +0 -0
  108. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/graph.py +0 -0
  109. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/importer/__init__.py +0 -0
  110. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/importer/base_importer.py +0 -0
  111. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/importer/import_graphml.py +0 -0
  112. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/importer/mapped_xlsx_importer.py +0 -0
  113. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/importer/qualia_importer.py +0 -0
  114. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/importer/unified_xlsx_importer.py +0 -0
  115. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/importer/xlsx_importer.py +0 -0
  116. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/indices.py +0 -0
  117. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/mappings/__init__.py +0 -0
  118. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/mappings/emdb/generic_specialfind_mapping.json +0 -0
  119. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/mappings/emdb/site_properties_mapping.json +0 -0
  120. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/mappings/emdb/usm_mapping.json +0 -0
  121. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/mappings/generic/excel_to_graphml_mapping.json +0 -0
  122. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/mappings/pyarchinit/pyarchinit_us_mapping.json +0 -0
  123. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/mappings/registry.py +0 -0
  124. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/mappings/template_emdb_mapping.json +0 -0
  125. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/mappings/template_pyarchinit_mapping.json +0 -0
  126. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/merge/__init__.py +0 -0
  127. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/merge/graph_merger.py +0 -0
  128. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/multigraph/__init__.py +0 -0
  129. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/multigraph/multigraph.py +0 -0
  130. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/__init__.py +0 -0
  131. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/author_node.py +0 -0
  132. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/base_node.py +0 -0
  133. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/combiner_node.py +0 -0
  134. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/document_node.py +0 -0
  135. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/embargo_node.py +0 -0
  136. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/epoch_node.py +0 -0
  137. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/extractor_node.py +0 -0
  138. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/geo_position_node.py +0 -0
  139. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/graph_node.py +0 -0
  140. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/group_node.py +0 -0
  141. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/hdt_node.py +0 -0
  142. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/license_node.py +0 -0
  143. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/link_node.py +0 -0
  144. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/paradata_node.py +0 -0
  145. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/property_node.py +0 -0
  146. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/representation_node.py +0 -0
  147. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/semantic_shape_node.py +0 -0
  148. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/nodes/stratigraphic_node.py +0 -0
  149. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/resolvers/__init__.py +0 -0
  150. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/resolvers/builtin_rules.py +0 -0
  151. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/resolvers/property_resolver.py +0 -0
  152. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/templates/em_data_template.xlsx +0 -0
  153. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/templates/em_palette_template.graphml +0 -0
  154. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/temporal/__init__.py +0 -0
  155. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/temporal/inference_engine.py +0 -0
  156. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/transforms/__init__.py +0 -0
  157. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/transforms/aux_tracking.py +0 -0
  158. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/transforms/compact.py +0 -0
  159. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/transforms/materialize_continuity.py +0 -0
  160. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/utils/__init__.py +0 -0
  161. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/utils/utils.py +0 -0
  162. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy/utils/visual_layout.py +0 -0
  163. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy.egg-info/dependency_links.txt +0 -0
  164. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/src/s3dgraphy.egg-info/top_level.txt +0 -0
  165. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/tests/test_composite_node_name.py +0 -0
  166. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/tests/test_filtered_import.py +0 -0
  167. {s3dgraphy-1.6.0.dev4 → s3dgraphy-1.6.0.dev6}/tests/test_lossless_roundtrip.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: s3dgraphy
3
- Version: 1.6.0.dev4
3
+ Version: 1.6.0.dev6
4
4
  Summary: 3D Stratigraphic Graph Management Library for archaeological and heritage applications
5
5
  Author-email: Emanuel Demetrescu <emanuel.demetrescu@cnr.it>
6
6
  Maintainer-email: Emanuel Demetrescu <emanuel.demetrescu@cnr.it>
@@ -43,6 +43,10 @@ Requires-Dist: pyoxigraph>=0.4; extra == "rdf-embedded"
43
43
  Provides-Extra: visualization
44
44
  Requires-Dist: matplotlib>=3.0; extra == "visualization"
45
45
  Requires-Dist: plotly>=5.0; extra == "visualization"
46
+ Provides-Extra: sync
47
+ Requires-Dist: sqlalchemy>=2.0; extra == "sync"
48
+ Provides-Extra: postgres
49
+ Requires-Dist: psycopg2-binary>=2.9; extra == "postgres"
46
50
  Provides-Extra: dev
47
51
  Requires-Dist: pytest>=7.0; extra == "dev"
48
52
  Requires-Dist: pytest-cov; extra == "dev"
@@ -57,9 +61,9 @@ Requires-Dist: sphinx>=5.0; extra == "docs"
57
61
  Requires-Dist: sphinx-rtd-theme; extra == "docs"
58
62
  Requires-Dist: myst-parser; extra == "docs"
59
63
  Provides-Extra: full
60
- Requires-Dist: s3dgraphy[rdf-embedded,visualization]; extra == "full"
64
+ Requires-Dist: s3dgraphy[rdf-embedded,sync,visualization]; extra == "full"
61
65
  Provides-Extra: all
62
- Requires-Dist: s3dgraphy[dev,docs,full]; extra == "all"
66
+ Requires-Dist: s3dgraphy[dev,docs,full,postgres]; extra == "all"
63
67
  Dynamic: license-file
64
68
 
65
69
  # s3dgraphy
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "s3dgraphy"
7
- version = "1.6.0.dev4"
7
+ version = "1.6.0.dev6"
8
8
  description = "3D Stratigraphic Graph Management Library for archaeological and heritage applications"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -92,6 +92,20 @@ visualization = [
92
92
  "matplotlib>=3.0",
93
93
  "plotly>=5.0",
94
94
  ]
95
+ sync = [
96
+ # Backend-agnostic SQL bridge (s3dgraphy.sync) — bidirectional
97
+ # ingestion + projection between a Graph and the host application's
98
+ # SQL tables. SQLite works out of the box; add the [postgres] extra
99
+ # for the PostgreSQL backend. Moved from pyArchInit in 1.6.0
100
+ # (zalmoxes-laran/s3Dgraphy#10).
101
+ "sqlalchemy>=2.0",
102
+ ]
103
+ postgres = [
104
+ # PostgreSQL backend driver. Combine with [sync] for the SQL bridge
105
+ # (zalmoxes-laran/s3Dgraphy#9 + #10). Standalone, it also enables
106
+ # the PG path of PyArchInitImporter when that lands.
107
+ "psycopg2-binary>=2.9",
108
+ ]
95
109
  dev = [
96
110
  "pytest>=7.0",
97
111
  "pytest-cov",
@@ -111,10 +125,10 @@ docs = [
111
125
  # without the dev/docs tooling. The dev/docs/test stacks stay separate
112
126
  # so CI-style installs don't pull editor/QA deps.
113
127
  full = [
114
- "s3dgraphy[rdf-embedded,visualization]",
128
+ "s3dgraphy[rdf-embedded,visualization,sync]",
115
129
  ]
116
130
  all = [
117
- "s3dgraphy[full,dev,docs]",
131
+ "s3dgraphy[full,dev,docs,postgres]",
118
132
  ]
119
133
 
120
134
  [tool.setuptools.packages.find]
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "formal_language": "EM",
3
- "version": "1.5.2",
3
+ "version": "1.5.3",
4
4
  "_changelog": {
5
+ "1.5.3": "Fix USD / serUSD material colour: RGB was (0.549, 0.103, 0.0) = #8C1A00 (red), should be #D86400 (orange) to match the canonical USD documentary colour used in the GraphML palette template, node_registry defaults and document_variant_styles.observable. USD border_color also corrected from #8C1A00 to #D86400 (serUSD border was already correct).",
5
6
  "1.5.2": "Added RSF (Reused Special Find / spolia) node style — octagon, red border (#9B3333), white fill. Sibling of SF/VSF, distinguished by border colour. Originating DP: DP-26.",
6
7
  "1.5.1": "Added LocationNodeGroup node style (dashed roundrectangle, kind-based border colour) and is_in_location edge style with primary_modifier override.",
7
8
  "1.5.0": "Initial EM 1.5 visual rules."
@@ -133,13 +134,13 @@
133
134
  "style": {
134
135
  "material": {
135
136
  "color": {
136
- "r": 0.549,
137
- "g": 0.103,
137
+ "r": 0.847,
138
+ "g": 0.392,
138
139
  "b": 0.0,
139
140
  "a": 1.0
140
141
  }
141
142
  },
142
- "border_color": "#8C1A00",
143
+ "border_color": "#D86400",
143
144
  "fill_color": "#F5F5F5",
144
145
  "border_style": "solid",
145
146
  "shape": "rounded_rectangle"
@@ -173,8 +174,8 @@
173
174
  "style": {
174
175
  "material": {
175
176
  "color": {
176
- "r": 0.549,
177
- "g": 0.103,
177
+ "r": 0.847,
178
+ "g": 0.392,
178
179
  "b": 0.0,
179
180
  "a": 1.0
180
181
  }
@@ -1,4 +1,4 @@
1
- __version__ = "1.6.0.dev4"
1
+ __version__ = "1.6.0.dev6"
2
2
  __datamodel_version__ = "1.5.5" # s3Dgraphy connections datamodel version
3
3
 
4
4
  # s3Dgraphy/__init__.py
@@ -236,33 +236,42 @@ class NodeRegistry:
236
236
  )
237
237
 
238
238
  # Sanity-check the canonical stratigraphic types are all present.
239
+ # Caller paths (NodeGenerator, GraphmlPatcher) already fall back to
240
+ # the 'US' stencil — or to inline defaults — when a single type is
241
+ # missing, so we only emit a diagnostic here.
239
242
  missing = sorted(_REQUIRED_PALETTE_TYPES - set(self.visual_properties))
240
243
  for em_type in missing:
241
244
  warnings.warn(
242
245
  f"s3dgraphy: palette template did not yield a stencil "
243
- f"for stratigraphic type {em_type!r}. Falling back to "
244
- "the hardcoded default visual properties. Verify that "
245
- "the palette template still contains the expected "
246
- "NodeLabel (USM01, USM02, USD10, USDxx ellipse for "
247
- "serUSD, USV100, USV102, USV106, SF01, VSF01, RSF01, "
248
- "TSU) or extend node_registry._PALETTE_DISPATCH_RULES.",
246
+ f"for stratigraphic type {em_type!r}. Callers will fall "
247
+ "back to the 'US' stencil. Verify that the palette "
248
+ "template still contains the expected NodeLabel (USM01, "
249
+ "USM02, USD10, USDxx ellipse for serUSD, USV100, USV102, "
250
+ "USV106, SF01, VSF01, RSF01, TSU) or extend "
251
+ "node_registry._PALETTE_DISPATCH_RULES.",
249
252
  S3DgraphyPaletteWarning,
250
253
  stacklevel=2,
251
254
  )
252
- if missing:
253
- # Backfill the missing ones from the hardcoded defaults so
254
- # callers never get None for a known stratigraphic type.
255
- defaults = self._default_visual_properties_dict()
256
- for em_type in missing:
257
- if em_type in defaults:
258
- self.visual_properties[em_type] = defaults[em_type]
259
255
 
260
256
  except (FileNotFoundError, ModuleNotFoundError) as e:
261
- print(f"[s3dgraphy] Warning: Palette template not found: {type(e).__name__}: {e}")
262
- self._load_default_visual_properties()
257
+ # The palette template is bundled with the package — its absence
258
+ # is a packaging/install bug, not a runtime condition we should
259
+ # paper over with hardcoded constants that can silently drift
260
+ # from the canonical palette. Fail loud.
261
+ raise RuntimeError(
262
+ "s3dgraphy: palette template "
263
+ "'templates/em_palette_template.graphml' is missing from "
264
+ f"the installed package ({type(e).__name__}: {e}). "
265
+ "GraphML export cannot proceed without it — reinstall "
266
+ "s3dgraphy or check the package data."
267
+ ) from e
263
268
  except Exception as e:
264
- print(f"[s3dgraphy] Warning: Error loading palette template: {e}")
265
- self._load_default_visual_properties()
269
+ # Same rationale: a corrupt template is a packaging issue, not
270
+ # something to silently swallow.
271
+ raise RuntimeError(
272
+ "s3dgraphy: failed to parse palette template "
273
+ f"'templates/em_palette_template.graphml': {e}"
274
+ ) from e
266
275
 
267
276
  def _extract_visual_properties(self, node_elem: ET.Element, ns: Dict) -> Optional[NodeVisualProperties]:
268
277
  """Extract visual properties from a palette node element."""
@@ -308,28 +317,6 @@ class NodeRegistry:
308
317
  print(f"Warning: Error extracting visual properties: {e}")
309
318
  return None
310
319
 
311
- @staticmethod
312
- def _default_visual_properties_dict() -> Dict[str, NodeVisualProperties]:
313
- """Hardcoded default visual properties (used as fallback)."""
314
- return {
315
- 'US': NodeVisualProperties('rectangle', '#FFFFFF', '#9B3333', 'line', 4.0, '#000000'),
316
- 'USVs': NodeVisualProperties('parallelogram', '#000000', '#248FE7', 'line', 4.0, '#FFFFFF'),
317
- 'USVn': NodeVisualProperties('hexagon', '#000000', '#31792D', 'line', 4.0, '#FFFFFF'),
318
- 'SF': NodeVisualProperties('octagon', '#FFFFFF', '#D8BD30', 'line', 4.0, '#000000'),
319
- 'VSF': NodeVisualProperties('octagon', '#000000', '#B19F61', 'line', 4.0, '#FFFFFF'),
320
- 'RSF': NodeVisualProperties('octagon', '#FFFFFF', '#9B3333', 'line', 4.0, '#000000'),
321
- 'USD': NodeVisualProperties('roundrectangle', '#FFFFFF', '#D86400', 'line', 4.0, '#000000'),
322
- 'serSU': NodeVisualProperties('ellipse', '#FFFFFF', '#9B3333', 'line', 4.0, '#000000'),
323
- 'serUSD': NodeVisualProperties('ellipse', '#FFFFFF', '#D86400', 'line', 4.0, '#000000'),
324
- 'serUSVn': NodeVisualProperties('ellipse', '#000000', '#31792D', 'line', 4.0, '#FFFFFF'),
325
- 'serUSVs': NodeVisualProperties('ellipse', '#000000', '#248FE7', 'line', 4.0, '#FFFFFF'),
326
- 'TSU': NodeVisualProperties('roundrectangle', '#FFFFFF', '#9B3333', 'dashed', 4.0, '#000000'),
327
- }
328
-
329
- def _load_default_visual_properties(self):
330
- """Load hardcoded default visual properties as fallback."""
331
- self.visual_properties = self._default_visual_properties_dict()
332
-
333
320
  def get_visual_properties(self, node_type: str) -> Optional[NodeVisualProperties]:
334
321
  """
335
322
  Get visual properties for a node type.
@@ -259,10 +259,45 @@ class _Datamodel:
259
259
  return _resolve_prefixed(cidoc), None, deprecated
260
260
 
261
261
  def get_qualia_crm_iri(self, property_type: Optional[str]) -> Optional[URIRef]:
262
+ """Resolve a property_type string to its CIDOC class IRI.
263
+
264
+ Lookup strategy (graceful, three steps):
265
+ 1. Exact match against em_qualia_types.json `id` (e.g.
266
+ "absolute_time_start", "height", "color").
267
+ 2. Last segment after dot — handles EM yEd convention where
268
+ properties are labelled with a category prefix
269
+ (e.g. "Dimension.height" → "height", "Spatial.elevation" →
270
+ "elevation").
271
+ 3. Lowercase match — handles minor case mismatches between
272
+ graphml labels and qualia ids (e.g. "Height" → "height").
273
+
274
+ Returns None if no strategy matches; the caller (typically
275
+ ``_compute_primary_iri``) falls back to the generic PropertyNode
276
+ default mapping.
277
+ """
262
278
  if not property_type:
263
279
  return None
280
+ # 1) Exact match
264
281
  crm = self._qualia_class_index.get(property_type)
265
- return _resolve_prefixed(crm)
282
+ if crm:
283
+ return _resolve_prefixed(crm)
284
+ # 2) Last segment after dot (yEd category prefix convention)
285
+ if "." in property_type:
286
+ tail = property_type.rsplit(".", 1)[-1]
287
+ crm = self._qualia_class_index.get(tail)
288
+ if crm:
289
+ return _resolve_prefixed(crm)
290
+ # 3) Lowercase fallback
291
+ crm = self._qualia_class_index.get(property_type.lower())
292
+ if crm:
293
+ return _resolve_prefixed(crm)
294
+ # 4) Combined: lowercase last segment
295
+ if "." in property_type:
296
+ tail_lower = property_type.rsplit(".", 1)[-1].lower()
297
+ crm = self._qualia_class_index.get(tail_lower)
298
+ if crm:
299
+ return _resolve_prefixed(crm)
300
+ return None
266
301
 
267
302
 
268
303
  # ─────────────────────────────────────────────────────────────────────────────
@@ -389,8 +424,22 @@ class RDFExporter:
389
424
  # ── path/format helpers ─────────────────────────────────────────────────
390
425
 
391
426
  def _adjust_extension(self, path: str) -> str:
427
+ """Ensure the file path ends with the format-correct extension.
428
+
429
+ Defensive against the leading-dot trap: a basename like ".ttl" is
430
+ treated by pathlib.Path as a hidden-file name (no suffix), so
431
+ ``with_suffix(".ttl")`` would produce ".ttl.ttl". We detect that case
432
+ and leave the path untouched if its name IS already the wanted ext.
433
+ """
392
434
  p = Path(path)
393
- if p.suffix.lstrip(".").lower() != self.ext:
435
+
436
+ # Leading-dot trap: basename equals "." + wanted ext (e.g. ".ttl")
437
+ # → treat as already correct, don't double-append.
438
+ if p.name.startswith('.') and p.name.lower().lstrip('.') == self.ext.lower():
439
+ return str(p)
440
+
441
+ current_ext = p.suffix.lstrip(".").lower()
442
+ if current_ext != self.ext.lower():
394
443
  return str(p.with_suffix("." + self.ext))
395
444
  return str(p)
396
445
 
@@ -507,23 +556,44 @@ class RDFExporter:
507
556
  Resolve the rdf:type primary IRI for a node, applying conditional rules.
508
557
 
509
558
  Conditional rule for PropertyNode:
510
- The qualia-type-specific class (looked up in em_qualia_types.json
511
- by `node.property_type`) takes precedence over the generic
512
- PropertyNode default class (typically crm:E54_Dimension).
513
- Without this, an aesthetic_value property would be typed as BOTH
514
- crm:E54_Dimension (PropertyNode default) and crminf:I4_Proposition_Set
515
- (qualia-specific), which is semantically misleading: aesthetic
516
- value is NOT a dimension.
517
-
518
- Fallback for all other node types and for PropertyNode with unknown
519
- property_type: the em_extension.uri or mapping.cidoc declared in the
520
- node datamodel.
559
+ The qualia-type-specific class (looked up in em_qualia_types.json)
560
+ takes precedence over the generic PropertyNode default class
561
+ (typically crm:E54_Dimension). Without this, an aesthetic_value
562
+ property would be typed as BOTH crm:E54_Dimension (PropertyNode
563
+ default) and crminf:I4_Proposition_Set (qualia-specific), which is
564
+ semantically misleading: aesthetic value is NOT a dimension.
565
+
566
+ Lookup key resolution (PropertyNode):
567
+ The s3dgraphy graphml importer preserves raw graphml data
568
+ (``node.name`` carries the NodeLabel, ``node.property_type`` is
569
+ the default "string" unless populated by the
570
+ ``_s3d_property_metadata`` side channel). To enrich at export
571
+ time without burdening the importer with vocabulary knowledge,
572
+ we try the lookup key in this order:
573
+ 1. ``node.property_type`` if explicitly set (not "string")
574
+ 2. ``node.name`` if available (the yEd NodeLabel — qualia
575
+ identifier in EM convention)
576
+ Either string is resolved through the multi-step graceful
577
+ matcher in ``_Datamodel.get_qualia_crm_iri`` (exact / dot-split
578
+ / lowercase). Falls back to the generic node datamodel mapping
579
+ when no qualia term matches (e.g. custom labels like
580
+ "lenght_pipe" stay as em:Qualia + crm:E1_CRM_Entity).
521
581
  """
522
582
  if node_type == "property":
523
583
  ptype = getattr(node, "property_type", None)
524
- qualia_iri = self.datamodel.get_qualia_crm_iri(ptype)
525
- if qualia_iri is not None:
526
- return qualia_iri
584
+ # Treat the default "string" sentinel as "unset" — the importer
585
+ # leaves it on the PropertyNode constructor default when no
586
+ # side-channel metadata is present.
587
+ if ptype and ptype.lower() != "string":
588
+ qualia_iri = self.datamodel.get_qualia_crm_iri(ptype)
589
+ if qualia_iri is not None:
590
+ return qualia_iri
591
+ # Fall back to NodeLabel (em yEd convention: label IS the qualia id)
592
+ name = getattr(node, "name", None)
593
+ if name:
594
+ qualia_iri = self.datamodel.get_qualia_crm_iri(name)
595
+ if qualia_iri is not None:
596
+ return qualia_iri
527
597
  return self.datamodel.get_node_primary_iri(cls_name)
528
598
 
529
599
  def _serialize_type_specific(self, node: Any, node_type: Optional[str],
@@ -533,10 +603,22 @@ class RDFExporter:
533
603
  if node_type == "property":
534
604
  # rdf:type already emitted by _compute_primary_iri (qualia-specific
535
605
  # class takes precedence over PropertyNode default).
606
+ #
607
+ # Value resolution: prefer node.value when set & non-empty;
608
+ # fall back to node.description for legacy graphml where the
609
+ # description data field encodes the value (yEd has no separate
610
+ # "value" socket on annotation-style PropertyNodes).
611
+ raw_value = getattr(node, "value", None)
612
+ if raw_value is None or (isinstance(raw_value, str) and not raw_value.strip()):
613
+ raw_value = getattr(node, "description", None)
614
+ if raw_value is not None and (not isinstance(raw_value, str) or raw_value.strip()):
615
+ ctx.add((node_iri, CRM.P90_has_value, Literal(raw_value)))
616
+
617
+ # Qualia type identifier — same key resolution as _compute_primary_iri:
618
+ # property_type if non-default, otherwise the NodeLabel (name).
536
619
  ptype = getattr(node, "property_type", None)
537
- value = getattr(node, "value", None)
538
- if value is not None:
539
- ctx.add((node_iri, CRM.P90_has_value, Literal(value)))
620
+ if not ptype or ptype.lower() == "string":
621
+ ptype = getattr(node, "name", None)
540
622
  if ptype:
541
623
  ctx.add((node_iri, EM.hasQualiaType, Literal(ptype)))
542
624
 
@@ -14,30 +14,100 @@ from ..multigraph.multigraph import multi_graph_manager
14
14
 
15
15
  # Conservative SQLite identifier whitelist: letters, digits, underscore.
16
16
  # Used to guard table names and filter column names interpolated into
17
- # query strings (values always go through ? parameter binding).
17
+ # query strings (values always go through paramstyle binding).
18
18
  _SAFE_IDENT_RE = re.compile(r'^[A-Za-z_][A-Za-z0-9_]*$')
19
19
 
20
+ # Recognized connection URL prefixes for dialect detection.
21
+ _PG_URL_PREFIXES = ("postgresql://", "postgresql+psycopg2://",
22
+ "postgres://")
23
+ _SQLITE_URL_PREFIX = "sqlite:///"
24
+
25
+
20
26
  class PyArchInitImporter(BaseImporter):
21
- def __init__(self, filepath: str, mapping_name: str, overwrite: bool = False,
22
- existing_graph=None, filters: Optional[Dict[str, Any]] = None):
27
+ def __init__(self, filepath: Optional[str] = None,
28
+ mapping_name: str = None, overwrite: bool = False,
29
+ existing_graph=None,
30
+ filters: Optional[Dict[str, Any]] = None,
31
+ *,
32
+ connection_url: Optional[str] = None):
23
33
  """
24
34
  Initialize pyArchInit importer with mapping configuration.
25
35
 
26
36
  Args:
27
- filepath: Path to the SQLite database
28
- mapping_name: Name of the JSON mapping file to use
29
- overwrite: If True, overwrites existing nodes
37
+ filepath: Path to a SQLite database (legacy / default path).
38
+ Mutually exclusive with ``connection_url``. When given,
39
+ it is internally promoted to ``sqlite:///<abspath>`` so
40
+ downstream code uses a single URL-based representation.
41
+ mapping_name: Name of the JSON mapping file to use.
42
+ overwrite: If True, overwrites existing nodes.
30
43
  existing_graph: Existing graph instance to use.
31
- If None, creates new unregistered graph with temporary ID.
32
- The caller (EM-tools) is responsible for setting proper graph_id
33
- and registering it in MultiGraphManager.
34
- filters: Optional dict of {column_name: value} to restrict the
35
- imported rows. Combined with AND. Each column is whitelisted
36
- against the mapping's column_mappings, then bound as a
37
- parameterized SQL value safe against injection.
44
+ If None, creates new unregistered graph with temporary
45
+ ID. The caller (EM-tools) is responsible for setting
46
+ proper graph_id and registering it in
47
+ MultiGraphManager.
48
+ filters: Optional dict of {column_name: value} to restrict
49
+ the imported rows. Combined with AND. Each column is
50
+ whitelisted against the mapping's column_mappings, then
51
+ bound as a parameterized SQL value — safe against
52
+ injection. Placeholder syntax adapts to the dialect
53
+ (``?`` on SQLite, ``%s`` on PostgreSQL).
54
+ connection_url: SQLAlchemy-style connection URL. Mutually
55
+ exclusive with ``filepath``. Supported schemes:
56
+ ``sqlite:///<abspath>``,
57
+ ``postgresql://user:pass@host:port/dbname`` (or the
58
+ ``postgres://`` alias / ``postgresql+psycopg2://`` form).
59
+ For PostgreSQL, ``psycopg2-binary`` must be installed
60
+ (``pip install s3dgraphy[postgres]``); a friendly
61
+ ``ImportError`` fires on first connection attempt if
62
+ it isn't.
63
+
64
+ Raises:
65
+ ValueError: If both ``filepath`` and ``connection_url`` are
66
+ given, if neither is given, or if ``connection_url``
67
+ uses an unsupported scheme.
38
68
  """
69
+ # Mutually exclusive + at-least-one validation.
70
+ if filepath is not None and connection_url is not None:
71
+ raise ValueError(
72
+ "Pass either filepath= or connection_url=, not both."
73
+ )
74
+ if filepath is None and connection_url is None:
75
+ raise ValueError(
76
+ "Either filepath= or connection_url= is required."
77
+ )
78
+
79
+ # Resolve dialect + canonical URL + the path-or-URL string we
80
+ # hand to BaseImporter as `filepath` (diagnostic-friendly).
81
+ if filepath is not None:
82
+ abs_path = os.path.abspath(filepath)
83
+ self._dialect = "sqlite"
84
+ self._connection_url = f"{_SQLITE_URL_PREFIX}{abs_path}"
85
+ _base_filepath = filepath
86
+ else:
87
+ if connection_url.startswith(_PG_URL_PREFIXES):
88
+ self._dialect = "postgres"
89
+ elif connection_url.startswith(_SQLITE_URL_PREFIX):
90
+ self._dialect = "sqlite"
91
+ else:
92
+ raise ValueError(
93
+ "Unsupported connection_url scheme: "
94
+ f"{connection_url!r}. "
95
+ "Use sqlite:///<path>, postgresql://..., "
96
+ "or postgres://..."
97
+ )
98
+ self._connection_url = connection_url
99
+ # BaseImporter uses filepath for diagnostics + abspath
100
+ # normalization. SQLite URLs reduce to a real path; PG URLs
101
+ # are passed through verbatim.
102
+ if self._dialect == "sqlite":
103
+ _base_filepath = connection_url[
104
+ len(_SQLITE_URL_PREFIX):
105
+ ]
106
+ else:
107
+ _base_filepath = connection_url
108
+
39
109
  super().__init__(
40
- filepath=filepath,
110
+ filepath=_base_filepath,
41
111
  mapping_name=mapping_name,
42
112
  overwrite=overwrite,
43
113
  filters=filters,
@@ -65,6 +135,61 @@ class PyArchInitImporter(BaseImporter):
65
135
 
66
136
  self.validate_mapping()
67
137
 
138
+ # ------------------------------------------------------------------
139
+ # Backend abstraction (#9 multi-backend)
140
+ # ------------------------------------------------------------------
141
+ def _connect(self):
142
+ """Open a DB-API 2 connection for the active dialect.
143
+
144
+ Returns a connection that the caller must close. For SQLite,
145
+ uses the stdlib ``sqlite3``. For PostgreSQL, uses
146
+ ``psycopg2`` and raises a friendly ``ImportError`` if the
147
+ extras are missing.
148
+ """
149
+ if self._dialect == "sqlite":
150
+ return sqlite3.connect(self.filepath)
151
+ # PostgreSQL path. Probe psycopg2 lazily so SQLite-only
152
+ # callers never pay the import cost (and don't need the wheel).
153
+ try:
154
+ import psycopg2 # noqa: F401
155
+ except ImportError as e: # pragma: no cover
156
+ raise ImportError(
157
+ "PostgreSQL backend requires psycopg2-binary. "
158
+ "Install via: pip install s3dgraphy[postgres]"
159
+ ) from e
160
+ import psycopg2
161
+ return psycopg2.connect(self._psycopg2_dsn())
162
+
163
+ def _psycopg2_dsn(self) -> str:
164
+ """Normalize the connection URL for ``psycopg2.connect()``.
165
+
166
+ ``psycopg2`` doesn't understand SQLAlchemy-style driver
167
+ suffixes (e.g. ``postgresql+psycopg2://``): it parses the
168
+ scheme literally and rejects the ``+psycopg2`` part with
169
+ "invalid dsn". The write side of the bridge
170
+ (``s3dgraphy.sync`` via SQLAlchemy) naturally produces those
171
+ URLs, so a caller wiring the *same* connection string into
172
+ both the read side (here) and the write side would otherwise
173
+ hit a silent failure on the read.
174
+
175
+ Stripping the ``+<driver>`` token lets one URL flow into both
176
+ without every caller having to know the dialect-prefix
177
+ convention. ``postgresql+psycopg2://`` → ``postgresql://`` and
178
+ ``postgres+psycopg2://`` → ``postgres://`` — both accepted by
179
+ psycopg2. Plain ``postgresql://`` / ``postgres://`` pass
180
+ through untouched.
181
+ """
182
+ url = self._connection_url
183
+ scheme, sep, rest = url.partition("://")
184
+ if sep and "+" in scheme:
185
+ scheme = scheme.split("+", 1)[0]
186
+ return f"{scheme}://{rest}"
187
+ return url
188
+
189
+ def _qmark(self) -> str:
190
+ """Parameter placeholder for the active dialect (``?`` / ``%s``)."""
191
+ return "?" if self._dialect == "sqlite" else "%s"
192
+
68
193
  def _resolve_node_name(self, row_dict: Dict[str, Any], id_column: str) -> str:
69
194
  """Compose the human-readable node name for ``row_dict``.
70
195
 
@@ -296,12 +421,13 @@ class PyArchInitImporter(BaseImporter):
296
421
 
297
422
  where_fragments = []
298
423
  params: List[Any] = []
424
+ qmark = self._qmark()
299
425
  for col, value in self.filters.items():
300
426
  # Defense in depth: validate against mapping + ident regex.
301
427
  self._validate_filter_column(col)
302
428
  if not self._is_safe_identifier(col):
303
429
  raise ValueError(f"Unsafe filter column name: {col!r}")
304
- where_fragments.append(f"{col} = ?")
430
+ where_fragments.append(f"{col} = {qmark}")
305
431
  params.append(value)
306
432
 
307
433
  where_clause = " AND ".join(where_fragments)
@@ -325,7 +451,7 @@ class PyArchInitImporter(BaseImporter):
325
451
  raise ValueError(f"Unsafe column name: {column!r}")
326
452
  table_name = self._get_table_name()
327
453
 
328
- conn = sqlite3.connect(self.filepath)
454
+ conn = self._connect()
329
455
  try:
330
456
  cursor = conn.cursor()
331
457
  cursor.execute(
@@ -340,7 +466,7 @@ class PyArchInitImporter(BaseImporter):
340
466
  """Parse pyArchInit database using mapping configuration"""
341
467
  try:
342
468
  # print("\n=== Starting PyArchInit Import ===")
343
- conn = sqlite3.connect(self.filepath)
469
+ conn = self._connect()
344
470
  cursor = conn.cursor()
345
471
 
346
472
  # Debug del mapping