structured2graph 0.2.0__tar.gz → 0.2.1__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.
- {structured2graph-0.2.0 → structured2graph-0.2.1}/.env +3 -3
- {structured2graph-0.2.0 → structured2graph-0.2.1}/.env.example +3 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/Dockerfile +7 -1
- {structured2graph-0.2.0 → structured2graph-0.2.1}/Dockerfile.local +6 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/PKG-INFO +1 -1
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/models/llm_models.py +24 -7
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/strategies/deterministic.py +41 -18
- {structured2graph-0.2.0 → structured2graph-0.2.1}/database/analyzer.py +13 -6
- {structured2graph-0.2.0 → structured2graph-0.2.1}/main.py +49 -32
- {structured2graph-0.2.0 → structured2graph-0.2.1}/pyproject.toml +1 -1
- structured2graph-0.2.1/skills/publish-dockerhub.md +13 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/utils/environment.py +26 -1
- {structured2graph-0.2.0 → structured2graph-0.2.1}/uv.lock +1 -1
- {structured2graph-0.2.0 → structured2graph-0.2.1}/.dockerignore +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/.gitignore +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/.python-version +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/LICENSE +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/PROMPT.md +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/README.md +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/__init__.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/__init__.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/__init__.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/hygm.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/models/__init__.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/models/graph_models.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/models/operations.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/models/sources.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/models/user_operations.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/strategies/__init__.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/strategies/base.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/strategies/llm.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/validation/__init__.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/validation/base.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/validation/graph_schema_validator.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/validation/memgraph_data_validator.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/migration_agent.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/schema/spec.json +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/core/utils/meta_graph.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/database/__init__.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/database/adapters/__init__.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/database/adapters/memgraph.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/database/adapters/mysql.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/database/adapters/postgresql.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/database/factory.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/database/models.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/examples/__init__.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/examples/basic_migration.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/examples/constraint_operations_example.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/query_generation/__init__.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/query_generation/cypher_generator.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/query_generation/schema_utilities.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/skills/running-under-docker-in-background.md +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/tests/__init__.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/tests/test_integration.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/utils/__init__.py +0 -0
- {structured2graph-0.2.0 → structured2graph-0.2.1}/utils/config.py +0 -0
|
@@ -23,7 +23,7 @@ ANTHROPIC_API_KEY="sk-ant-api03-vXCucqmmzT44s_YtHgbq_Y7-aHlFC2YwRplQeWi_nhvzbvkk
|
|
|
23
23
|
# SQL2MG_LOG_LEVEL=INFO
|
|
24
24
|
|
|
25
25
|
# Source Database Selection - options: mysql, postgresql
|
|
26
|
-
SOURCE_DB_TYPE=
|
|
26
|
+
SOURCE_DB_TYPE=mysql
|
|
27
27
|
|
|
28
28
|
# # MySQL Database Configuration (used when SOURCE_DB_TYPE=mysql)
|
|
29
29
|
MYSQL_HOST=localhost
|
|
@@ -33,8 +33,8 @@ MYSQL_DATABASE=employees
|
|
|
33
33
|
MYSQL_PORT=3306
|
|
34
34
|
|
|
35
35
|
# PostgreSQL Database Configuration (used when SOURCE_DB_TYPE=postgresql)
|
|
36
|
-
|
|
37
|
-
POSTGRES_HOST=postgres-dev
|
|
36
|
+
POSTGRES_HOST=localhost
|
|
37
|
+
# POSTGRES_HOST=postgres-dev
|
|
38
38
|
POSTGRES_USER=postgres
|
|
39
39
|
POSTGRES_PASSWORD=postgres
|
|
40
40
|
POSTGRES_DATABASE=postgres
|
|
@@ -2,11 +2,17 @@ FROM python:3.12-slim
|
|
|
2
2
|
|
|
3
3
|
WORKDIR /app
|
|
4
4
|
|
|
5
|
+
# Install editors for interactive mapping editing
|
|
6
|
+
RUN apt-get update && apt-get install -y --no-install-recommends vim-tiny neovim \
|
|
7
|
+
&& ln -s /usr/bin/vim.tiny /usr/bin/vi \
|
|
8
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
9
|
+
ENV EDITOR=nvim
|
|
10
|
+
|
|
5
11
|
# Install uv for fast dependency management
|
|
6
12
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
|
7
13
|
|
|
8
14
|
# Install the released package from PyPI
|
|
9
|
-
RUN uv pip install --system structured2graph
|
|
15
|
+
RUN uv pip install --system structured2graph==0.2.1
|
|
10
16
|
|
|
11
17
|
# Copy .env.example as reference; users supply real values at runtime
|
|
12
18
|
# via: docker run -it --env-file .env memgraph/structured2graph
|
|
@@ -2,6 +2,12 @@ FROM python:3.12-slim
|
|
|
2
2
|
|
|
3
3
|
WORKDIR /app
|
|
4
4
|
|
|
5
|
+
# Install editors for interactive mapping editing
|
|
6
|
+
RUN apt-get update && apt-get install -y --no-install-recommends vim-tiny neovim \
|
|
7
|
+
&& ln -s /usr/bin/vim.tiny /usr/bin/vi \
|
|
8
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
9
|
+
ENV EDITOR=nvim
|
|
10
|
+
|
|
5
11
|
# Install uv for fast dependency management
|
|
6
12
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
|
7
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structured2graph
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Database migration agent from structured data (e.g. SQL) to graph.
|
|
5
5
|
Project-URL: Homepage, https://github.com/memgraph/ai-toolkit
|
|
6
6
|
Project-URL: Repository, https://github.com/memgraph/ai-toolkit
|
|
@@ -165,10 +165,17 @@ class LLMGraphModel(BaseModel):
|
|
|
165
165
|
)
|
|
166
166
|
node_constraints.append(graph_constraint)
|
|
167
167
|
|
|
168
|
+
# Build a lookup from node name → source table
|
|
169
|
+
node_table_map = {}
|
|
170
|
+
node_pk_map = {}
|
|
171
|
+
for node in self.nodes:
|
|
172
|
+
node_table_map[node.name] = node.source_table
|
|
173
|
+
node_pk_map[node.name] = node.primary_key
|
|
174
|
+
|
|
168
175
|
# Convert relationships
|
|
169
176
|
graph_relationships = []
|
|
170
177
|
for llm_rel in self.relationships:
|
|
171
|
-
# Find source/target node labels
|
|
178
|
+
# Find source/target node labels and tables
|
|
172
179
|
from_labels = []
|
|
173
180
|
to_labels = []
|
|
174
181
|
for node in self.nodes:
|
|
@@ -177,6 +184,16 @@ class LLMGraphModel(BaseModel):
|
|
|
177
184
|
if node.name == llm_rel.to_node:
|
|
178
185
|
to_labels = node.labels
|
|
179
186
|
|
|
187
|
+
from_table = node_table_map.get(llm_rel.from_node, "")
|
|
188
|
+
to_table = node_table_map.get(llm_rel.to_node, "")
|
|
189
|
+
from_pk = node_pk_map.get(llm_rel.from_node, "id")
|
|
190
|
+
to_pk = node_pk_map.get(llm_rel.to_node, "id")
|
|
191
|
+
|
|
192
|
+
# For one-to-many the FK lives in the "from" side's table;
|
|
193
|
+
# use the target table as the source table name.
|
|
194
|
+
# Fall back to the from_table when we can't determine better.
|
|
195
|
+
source_table = to_table or from_table
|
|
196
|
+
|
|
180
197
|
# Create relationship properties
|
|
181
198
|
rel_properties = []
|
|
182
199
|
for prop_name in llm_rel.properties:
|
|
@@ -188,14 +205,14 @@ class LLMGraphModel(BaseModel):
|
|
|
188
205
|
)
|
|
189
206
|
rel_properties.append(rel_prop)
|
|
190
207
|
|
|
191
|
-
# Create relationship source
|
|
208
|
+
# Create relationship source with actual SQL table/column info
|
|
192
209
|
rel_source = RelationshipSource(
|
|
193
|
-
type="
|
|
194
|
-
name=
|
|
195
|
-
location=f"
|
|
210
|
+
type="table",
|
|
211
|
+
name=source_table,
|
|
212
|
+
location=f"database.schema.{source_table}",
|
|
196
213
|
mapping={
|
|
197
|
-
"start_node":
|
|
198
|
-
"end_node":
|
|
214
|
+
"start_node": f"{from_table}.{from_pk}",
|
|
215
|
+
"end_node": f"{to_table}.{to_pk}",
|
|
199
216
|
"edge_type": llm_rel.name,
|
|
200
217
|
},
|
|
201
218
|
)
|
|
@@ -147,17 +147,39 @@ class DeterministicStrategy(BaseModelingStrategy):
|
|
|
147
147
|
primary_keys = from_table_info.get("primary_keys", [])
|
|
148
148
|
from_pk = primary_keys[0] if primary_keys else f"{from_table}_id"
|
|
149
149
|
|
|
150
|
+
# Use the join table name for many-to-many, otherwise the from_table
|
|
151
|
+
join_table = rel_data.get("join_table", "")
|
|
152
|
+
source_table = join_table or from_table
|
|
153
|
+
|
|
154
|
+
# Build column references
|
|
155
|
+
if join_table:
|
|
156
|
+
# Many-to-many: both FK columns live in the join table
|
|
157
|
+
from_col = rel_data.get("join_from_column", "id")
|
|
158
|
+
to_col = rel_data.get("join_to_column", "id")
|
|
159
|
+
start_ref = f"{join_table}.{from_col}"
|
|
160
|
+
end_ref = f"{join_table}.{to_col}"
|
|
161
|
+
else:
|
|
162
|
+
# One-to-many: from_col is in from_table, to_col is in to_table
|
|
163
|
+
from_col = rel_data.get("from_column", "id")
|
|
164
|
+
to_col = rel_data.get("to_column", "id")
|
|
165
|
+
start_ref = f"{from_table}.{from_col}"
|
|
166
|
+
end_ref = f"{to_table}.{to_col}"
|
|
167
|
+
|
|
150
168
|
# Create relationship source
|
|
169
|
+
mapping = {
|
|
170
|
+
"start_node": start_ref,
|
|
171
|
+
"end_node": end_ref,
|
|
172
|
+
"edge_type": rel_name,
|
|
173
|
+
"from_pk": from_pk,
|
|
174
|
+
}
|
|
175
|
+
if join_table:
|
|
176
|
+
mapping["join_table"] = join_table
|
|
177
|
+
|
|
151
178
|
rel_source = RelationshipSource(
|
|
152
179
|
type="table",
|
|
153
|
-
name=
|
|
154
|
-
location=f"database.schema.{
|
|
155
|
-
mapping=
|
|
156
|
-
"start_node": (f"{from_table}.{rel_data.get('from_column', 'id')}"),
|
|
157
|
-
"end_node": (f"{to_table}.{rel_data.get('to_column', 'id')}"),
|
|
158
|
-
"edge_type": rel_name,
|
|
159
|
-
"from_pk": from_pk, # Add primary key for migration agent
|
|
160
|
-
},
|
|
180
|
+
name=source_table,
|
|
181
|
+
location=f"database.schema.{source_table}",
|
|
182
|
+
mapping=mapping,
|
|
161
183
|
)
|
|
162
184
|
|
|
163
185
|
# Create relationship
|
|
@@ -244,19 +266,20 @@ class DeterministicStrategy(BaseModelingStrategy):
|
|
|
244
266
|
|
|
245
267
|
def _generate_relationship_name(self, rel_data: Dict[str, Any]) -> str:
|
|
246
268
|
"""Generate a semantic relationship name from relationship data."""
|
|
269
|
+
# For many-to-many, use the join table name directly
|
|
270
|
+
join_table = rel_data.get("join_table", "")
|
|
271
|
+
if join_table:
|
|
272
|
+
return join_table.upper()
|
|
273
|
+
|
|
247
274
|
constraint_name = rel_data.get("constraint_name", "")
|
|
248
275
|
if constraint_name:
|
|
249
276
|
# Extract meaningful name from constraint
|
|
250
277
|
if "_fk" in constraint_name:
|
|
251
|
-
|
|
278
|
+
name = constraint_name.split("_fk")[0]
|
|
252
279
|
else:
|
|
253
|
-
|
|
280
|
+
name = constraint_name
|
|
281
|
+
return name.upper() if name else "CONNECTS"
|
|
254
282
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return "CONNECTS"
|
|
259
|
-
else:
|
|
260
|
-
from_table = rel_data.get("from_table", "")
|
|
261
|
-
to_table = rel_data.get("to_table", "")
|
|
262
|
-
return f"{from_table.upper()}_TO_{to_table.upper()}"
|
|
283
|
+
from_table = rel_data.get("from_table", "")
|
|
284
|
+
to_table = rel_data.get("to_table", "")
|
|
285
|
+
return f"{from_table.upper()}_TO_{to_table.upper()}"
|
|
@@ -212,21 +212,28 @@ class DatabaseAnalyzer(ABC):
|
|
|
212
212
|
if len(table_info.foreign_keys) < 2:
|
|
213
213
|
return False
|
|
214
214
|
|
|
215
|
-
# Count non-FK columns (excluding common metadata columns
|
|
215
|
+
# Count non-FK columns (excluding common metadata / temporal columns
|
|
216
|
+
# that often appear on join tables as relationship properties)
|
|
216
217
|
non_fk_columns = []
|
|
217
218
|
fk_column_names = {fk.column_name for fk in table_info.foreign_keys}
|
|
218
|
-
|
|
219
|
+
auxiliary_columns = {
|
|
219
220
|
"id",
|
|
220
221
|
"created_at",
|
|
221
222
|
"updated_at",
|
|
222
223
|
"created_on",
|
|
223
224
|
"updated_on",
|
|
224
225
|
"timestamp",
|
|
226
|
+
"from_date",
|
|
227
|
+
"to_date",
|
|
228
|
+
"start_date",
|
|
229
|
+
"end_date",
|
|
230
|
+
"valid_from",
|
|
231
|
+
"valid_to",
|
|
225
232
|
}
|
|
226
233
|
|
|
227
234
|
for col in table_info.columns:
|
|
228
235
|
field_name = col.name.lower()
|
|
229
|
-
if col.name not in fk_column_names and field_name not in
|
|
236
|
+
if col.name not in fk_column_names and field_name not in auxiliary_columns:
|
|
230
237
|
non_fk_columns.append(col.name)
|
|
231
238
|
|
|
232
239
|
# If most columns are foreign keys, it's likely a join table
|
|
@@ -234,9 +241,9 @@ class DatabaseAnalyzer(ABC):
|
|
|
234
241
|
fk_ratio = len(table_info.foreign_keys) / total_columns
|
|
235
242
|
|
|
236
243
|
# Consider it a join table if:
|
|
237
|
-
# - At least 2 FKs and FK ratio
|
|
238
|
-
# - All columns are FKs or
|
|
239
|
-
return (len(table_info.foreign_keys) >= 2 and fk_ratio
|
|
244
|
+
# - At least 2 FKs and FK ratio >= 0.5, OR
|
|
245
|
+
# - All columns are FKs or auxiliary columns
|
|
246
|
+
return (len(table_info.foreign_keys) >= 2 and fk_ratio >= 0.5) or len(
|
|
240
247
|
non_fk_columns
|
|
241
248
|
) == 0
|
|
242
249
|
|
|
@@ -59,6 +59,9 @@ LOG_LEVEL_CHOICES = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"]
|
|
|
59
59
|
|
|
60
60
|
PROVIDER_CHOICES = ["openai", "anthropic", "gemini"]
|
|
61
61
|
|
|
62
|
+
# Sentinel returned by edit_mapping_interactive to signal a reset
|
|
63
|
+
_RESET = object()
|
|
64
|
+
|
|
62
65
|
|
|
63
66
|
def _lower_env(name: str) -> Optional[str]:
|
|
64
67
|
value = os.getenv(name)
|
|
@@ -783,6 +786,7 @@ def edit_mapping_interactive(
|
|
|
783
786
|
print(" /edit - open mapping in " + editor)
|
|
784
787
|
print(" /save - save and exit")
|
|
785
788
|
print(" /cancel - discard changes and exit")
|
|
789
|
+
print(" /reset - discard mapping and regenerate from scratch")
|
|
786
790
|
print("\nOr describe changes in natural language (sent to LLM), e.g.:")
|
|
787
791
|
print(" Add a Person label node mapped from the people table")
|
|
788
792
|
print(" Rename label Person to User")
|
|
@@ -812,6 +816,8 @@ def edit_mapping_interactive(
|
|
|
812
816
|
print("Mapping updated from editor.")
|
|
813
817
|
print_mapping_summary(mapping)
|
|
814
818
|
_print_editor_banner()
|
|
819
|
+
elif cmd == "reset":
|
|
820
|
+
return _RESET
|
|
815
821
|
else:
|
|
816
822
|
print(f"Unknown command: {user_input}")
|
|
817
823
|
continue
|
|
@@ -851,34 +857,14 @@ def edit_mapping_interactive(
|
|
|
851
857
|
return mapping
|
|
852
858
|
|
|
853
859
|
|
|
854
|
-
def
|
|
860
|
+
def _generate_fresh_mapping(
|
|
855
861
|
agent: SQLToMemgraphAgent,
|
|
856
862
|
source_db_config: Dict[str, Any],
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
"""
|
|
860
|
-
Generate or edit a mapping file.
|
|
861
|
-
|
|
862
|
-
If the file already exists it is loaded and the user enters an interactive
|
|
863
|
-
editing session. Otherwise the source database is analysed and a new
|
|
864
|
-
mapping is created from scratch.
|
|
865
|
-
"""
|
|
866
|
-
output = Path(mapping_path)
|
|
867
|
-
|
|
868
|
-
# If mapping already exists, load it and enter edit mode
|
|
869
|
-
if output.exists():
|
|
870
|
-
with open(output, "r", encoding="utf-8") as f:
|
|
871
|
-
mapping = json.load(f)
|
|
872
|
-
|
|
873
|
-
print(f"📄 Loaded existing mapping from {output}")
|
|
874
|
-
print_mapping_summary(mapping)
|
|
875
|
-
edit_mapping_interactive(mapping, agent.llm, mapping_path)
|
|
876
|
-
return
|
|
877
|
-
|
|
863
|
+
) -> Dict[str, Any]:
|
|
864
|
+
"""Analyse the source database and return a new mapping dict."""
|
|
878
865
|
from database.factory import DatabaseAnalyzerFactory
|
|
879
866
|
from core.hygm import HyGM
|
|
880
867
|
|
|
881
|
-
# 1. Connect and analyse the source database
|
|
882
868
|
print("🔍 Analyzing source database schema...")
|
|
883
869
|
config = source_db_config.copy()
|
|
884
870
|
db_type = config.pop("database_type", "mysql")
|
|
@@ -891,7 +877,6 @@ def generate_mapping(
|
|
|
891
877
|
analyzer.disconnect()
|
|
892
878
|
print(f" Found {len(hygm_data.get('entity_tables', {}))} entity tables")
|
|
893
879
|
|
|
894
|
-
# 2. Build the graph model via HyGM
|
|
895
880
|
print("🎯 Creating graph model...")
|
|
896
881
|
graph_modeler = HyGM(
|
|
897
882
|
llm=agent.llm,
|
|
@@ -907,16 +892,48 @@ def generate_mapping(
|
|
|
907
892
|
f"{len(graph_model.edges)} relationship types"
|
|
908
893
|
)
|
|
909
894
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
895
|
+
return graph_model_to_mapping(graph_model)
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
def generate_mapping(
|
|
899
|
+
agent: SQLToMemgraphAgent,
|
|
900
|
+
source_db_config: Dict[str, Any],
|
|
901
|
+
mapping_path: str,
|
|
902
|
+
) -> None:
|
|
903
|
+
"""
|
|
904
|
+
Generate or edit a mapping file.
|
|
905
|
+
|
|
906
|
+
If the file already exists it is loaded and the user enters an interactive
|
|
907
|
+
editing session. Otherwise the source database is analysed and a new
|
|
908
|
+
mapping is created from scratch. The user can type /reset at any point
|
|
909
|
+
to discard the mapping and regenerate from the source database.
|
|
910
|
+
"""
|
|
911
|
+
output = Path(mapping_path)
|
|
912
|
+
|
|
913
|
+
if output.exists():
|
|
914
|
+
with open(output, "r", encoding="utf-8") as f:
|
|
915
|
+
mapping = json.load(f)
|
|
916
|
+
print(f"📄 Loaded existing mapping from {output}")
|
|
917
|
+
else:
|
|
918
|
+
mapping = _generate_fresh_mapping(agent, source_db_config)
|
|
919
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
920
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
921
|
+
json.dump(mapping, f, indent=2)
|
|
922
|
+
print(f"\n📄 Mapping file written to {output}")
|
|
923
|
+
|
|
916
924
|
print_mapping_summary(mapping)
|
|
917
925
|
|
|
918
|
-
|
|
919
|
-
|
|
926
|
+
while True:
|
|
927
|
+
result = edit_mapping_interactive(mapping, agent.llm, mapping_path)
|
|
928
|
+
if result is not _RESET:
|
|
929
|
+
break
|
|
930
|
+
print("\n🔄 Resetting mapping — regenerating from source database...\n")
|
|
931
|
+
mapping = _generate_fresh_mapping(agent, source_db_config)
|
|
932
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
933
|
+
with open(output, "w", encoding="utf-8") as f:
|
|
934
|
+
json.dump(mapping, f, indent=2)
|
|
935
|
+
print(f"\n📄 Mapping file written to {output}")
|
|
936
|
+
print_mapping_summary(mapping)
|
|
920
937
|
|
|
921
938
|
|
|
922
939
|
def main(argv: Optional[list[str]] = None) -> None:
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Build and publish the structured2graph Docker image to Docker Hub
|
|
3
|
+
user-invocable: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Read the current version from `pyproject.toml` (the `version` field under `[project]`).
|
|
7
|
+
|
|
8
|
+
Build and push multi-platform (amd64 + arm64) with both the version tag and `latest`:
|
|
9
|
+
```bash
|
|
10
|
+
docker buildx build --platform linux/amd64,linux/arm64 -t memgraph/structured2graph:<version> -t memgraph/structured2graph:latest --push .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Replace `<version>` with the actual version from `pyproject.toml`.
|
|
@@ -42,9 +42,34 @@ def _strip_quotes_from_env() -> None:
|
|
|
42
42
|
os.environ[key] = val[1:-1]
|
|
43
43
|
|
|
44
44
|
|
|
45
|
+
def _find_dotenv() -> Optional[str]:
|
|
46
|
+
"""Search well-known locations for a .env file.
|
|
47
|
+
|
|
48
|
+
Returns the first path that exists, or None. The search order is:
|
|
49
|
+
1. Current working directory
|
|
50
|
+
2. /app (Docker WORKDIR convention)
|
|
51
|
+
3. Directory containing this source file (dev checkout)
|
|
52
|
+
"""
|
|
53
|
+
candidates = [
|
|
54
|
+
os.path.join(os.getcwd(), ".env"),
|
|
55
|
+
"/app/.env",
|
|
56
|
+
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env"),
|
|
57
|
+
]
|
|
58
|
+
for path in candidates:
|
|
59
|
+
if os.path.isfile(path):
|
|
60
|
+
return path
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
45
64
|
def load_environment() -> None:
|
|
46
65
|
"""Load environment variables from .env file."""
|
|
47
|
-
|
|
66
|
+
dotenv_path = _find_dotenv()
|
|
67
|
+
if dotenv_path:
|
|
68
|
+
logger.info("Loading .env from %s", dotenv_path)
|
|
69
|
+
load_dotenv(dotenv_path)
|
|
70
|
+
else:
|
|
71
|
+
# Fall back to default find_dotenv() behaviour
|
|
72
|
+
load_dotenv()
|
|
48
73
|
_strip_quotes_from_env()
|
|
49
74
|
|
|
50
75
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/validation/graph_schema_validator.py
RENAMED
|
File without changes
|
{structured2graph-0.2.0 → structured2graph-0.2.1}/core/hygm/validation/memgraph_data_validator.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{structured2graph-0.2.0 → structured2graph-0.2.1}/skills/running-under-docker-in-background.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|