hinge 0.1.0__py3-none-any.whl
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.
- hinge/__init__.py +3 -0
- hinge/config/__init__.py +0 -0
- hinge/config/logging_setup.py +63 -0
- hinge/config/settings.py +23 -0
- hinge/config/types.yaml +42 -0
- hinge/dbt/.user.yml +1 -0
- hinge/dbt/dbt_project.yml +18 -0
- hinge/dbt/macros/apply_time_window.sql +11 -0
- hinge/dbt/macros/assert_recipe_supported.sql +59 -0
- hinge/dbt/macros/build_incidence.sql +46 -0
- hinge/dbt/macros/collapse_path.sql +26 -0
- hinge/dbt/macros/filter_bots.sql +23 -0
- hinge/dbt/macros/network_edges.sql +39 -0
- hinge/dbt/macros/project_bipartite.sql +33 -0
- hinge/dbt/macros/slice_edges.sql +41 -0
- hinge/dbt/models/hin/hin_accounts.sql +14 -0
- hinge/dbt/models/hin/hin_artifacts.sql +23 -0
- hinge/dbt/models/hin/hin_capabilities.sql +42 -0
- hinge/dbt/models/hin/hin_edges.sql +57 -0
- hinge/dbt/models/hin/hin_nodes.sql +44 -0
- hinge/dbt/models/hin/hin_repositories.sql +15 -0
- hinge/dbt/models/intermediate/int_developer_repo_affiliation.sql +27 -0
- hinge/dbt/models/intermediate/int_user_artifact_incidence.sql +21 -0
- hinge/dbt/models/networks/artifact_reference.sql +27 -0
- hinge/dbt/models/networks/co_commit_user_user.sql +48 -0
- hinge/dbt/models/networks/co_edit_file_user_user.sql +48 -0
- hinge/dbt/models/networks/co_edit_line_user_user.sql +48 -0
- hinge/dbt/models/networks/dev_interaction.sql +98 -0
- hinge/dbt/models/networks/developer_repo_affiliation.sql +20 -0
- hinge/dbt/models/networks/follow_user_user.sql +27 -0
- hinge/dbt/models/networks/fork_repo_repo.sql +27 -0
- hinge/dbt/models/networks/issue_co_participation.sql +50 -0
- hinge/dbt/models/networks/issue_participation.sql +40 -0
- hinge/dbt/models/networks/pr_author_reviewer.sql +66 -0
- hinge/dbt/models/networks/pr_participation.sql +40 -0
- hinge/dbt/models/networks/pr_reviewer_coreview.sql +48 -0
- hinge/dbt/models/networks/repo_shared_contributors.sql +41 -0
- hinge/dbt/models/networks/star_user_repo.sql +27 -0
- hinge/dbt/models/networks/user_mention_user.sql +60 -0
- hinge/dbt/models/networks/watch_user_repo.sql +27 -0
- hinge/dbt/models/sources/sources.yml +63 -0
- hinge/dbt/models/staging/stg_accounts.sql +4 -0
- hinge/dbt/models/staging/stg_adapter_manifest.sql +4 -0
- hinge/dbt/models/staging/stg_artifacts.sql +4 -0
- hinge/dbt/models/staging/stg_relations.sql +4 -0
- hinge/dbt/models/staging/stg_repositories.sql +4 -0
- hinge/dbt/profiles.yml +9 -0
- hinge/dbt/seeds/ref_recipe_requirements.csv +35 -0
- hinge/frontends/__init__.py +0 -0
- hinge/frontends/cli/__init__.py +3 -0
- hinge/frontends/cli/app.py +189 -0
- hinge/frontends/lib.py +148 -0
- hinge/frontends/tui/__init__.py +7 -0
- hinge/frontends/web/__init__.py +6 -0
- hinge/kernel/__init__.py +0 -0
- hinge/kernel/projection/__init__.py +9 -0
- hinge/kernel/projection/projected_graph.py +32 -0
- hinge/kernel/projection/projection_spec.py +35 -0
- hinge/kernel/protocols/__init__.py +24 -0
- hinge/kernel/protocols/dataset_view.py +30 -0
- hinge/kernel/protocols/exporter_stage.py +24 -0
- hinge/kernel/protocols/projection_stage.py +36 -0
- hinge/kernel/protocols/reader_stage.py +52 -0
- hinge/kernel/protocols/store_stage.py +71 -0
- hinge/kernel/registry.py +71 -0
- hinge/kernel/runner.py +236 -0
- hinge/kernel/schema/__init__.py +12 -0
- hinge/kernel/schema/hin_schema.py +38 -0
- hinge/kernel/schema/schema_violation.py +21 -0
- hinge/kernel/schema/typed_edge.py +20 -0
- hinge/kernel/schema/typed_node.py +17 -0
- hinge/stages/__init__.py +0 -0
- hinge/stages/exporters/__init__.py +0 -0
- hinge/stages/exporters/csv_exporter.py +77 -0
- hinge/stages/exporters/dot_exporter.py +13 -0
- hinge/stages/exporters/gexf_exporter.py +13 -0
- hinge/stages/exporters/gml_exporter.py +79 -0
- hinge/stages/exporters/graphml_exporter.py +13 -0
- hinge/stages/exporters/jsonld_exporter.py +13 -0
- hinge/stages/exporters/networkx_exporter.py +57 -0
- hinge/stages/exporters/parquet_exporter.py +13 -0
- hinge/stages/projection/__init__.py +0 -0
- hinge/stages/projection/dbt_projection.py +342 -0
- hinge/stages/projection/specs/__init__.py +0 -0
- hinge/stages/projection/specs/artifact_reference.py +14 -0
- hinge/stages/projection/specs/co_commit_user_user.py +14 -0
- hinge/stages/projection/specs/co_edit_file_user_user.py +14 -0
- hinge/stages/projection/specs/co_edit_line_user_user.py +14 -0
- hinge/stages/projection/specs/dev_interaction.py +26 -0
- hinge/stages/projection/specs/developer_repo_affiliation.py +15 -0
- hinge/stages/projection/specs/follow_user_user.py +14 -0
- hinge/stages/projection/specs/fork_repo_repo.py +14 -0
- hinge/stages/projection/specs/issue_co_participation.py +14 -0
- hinge/stages/projection/specs/issue_participation.py +14 -0
- hinge/stages/projection/specs/pr_author_reviewer.py +14 -0
- hinge/stages/projection/specs/pr_participation.py +14 -0
- hinge/stages/projection/specs/pr_reviewer_coreview.py +14 -0
- hinge/stages/projection/specs/repo_shared_contributors.py +14 -0
- hinge/stages/projection/specs/star_user_repo.py +15 -0
- hinge/stages/projection/specs/user_mention_user.py +14 -0
- hinge/stages/projection/specs/watch_user_repo.py +14 -0
- hinge/stages/readers/__init__.py +0 -0
- hinge/stages/readers/_numfocus_contract_adapter.py +314 -0
- hinge/stages/readers/numfocus_reader.py +69 -0
- hinge/stages/store/__init__.py +0 -0
- hinge/stages/store/duckdb_store.py +466 -0
- hinge-0.1.0.dist-info/METADATA +309 -0
- hinge-0.1.0.dist-info/RECORD +111 -0
- hinge-0.1.0.dist-info/WHEEL +4 -0
- hinge-0.1.0.dist-info/entry_points.txt +35 -0
- hinge-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{{ config(materialized='table') }}
|
|
2
|
+
|
|
3
|
+
{{ assert_recipe_supported('star_user_repo') }}
|
|
4
|
+
|
|
5
|
+
WITH starred_edges AS (
|
|
6
|
+
{{ slice_edges(
|
|
7
|
+
edge_types=['starred'],
|
|
8
|
+
source_types=['user'],
|
|
9
|
+
target_types=['repo']
|
|
10
|
+
) }}
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
{{ network_edges(
|
|
14
|
+
relation='starred_edges',
|
|
15
|
+
recipe_name='star_user_repo',
|
|
16
|
+
source_node_id='source_node_id',
|
|
17
|
+
source_node_type="'user'",
|
|
18
|
+
target_node_id='target_node_id',
|
|
19
|
+
target_node_type="'repo'",
|
|
20
|
+
edge_type="'starred'",
|
|
21
|
+
directed='true',
|
|
22
|
+
weight='coalesce(weight, 1.0)',
|
|
23
|
+
weight_kind="'binary'",
|
|
24
|
+
first_seen_at='occurred_at',
|
|
25
|
+
last_seen_at='occurred_at',
|
|
26
|
+
properties="to_json({'source_record_id': source_record_id})"
|
|
27
|
+
) }}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{{ config(materialized='table') }}
|
|
2
|
+
|
|
3
|
+
{{ assert_recipe_supported('user_mention_user') }}
|
|
4
|
+
|
|
5
|
+
WITH authored_comments AS (
|
|
6
|
+
SELECT
|
|
7
|
+
source_node_id AS author_node_id,
|
|
8
|
+
target_node_id AS comment_node_id,
|
|
9
|
+
min(occurred_at) AS first_seen_at,
|
|
10
|
+
max(occurred_at) AS last_seen_at
|
|
11
|
+
FROM {{ ref('hin_edges') }}
|
|
12
|
+
WHERE source_node_type = 'user'
|
|
13
|
+
AND target_node_type = 'artifact'
|
|
14
|
+
AND target_node_subtype IN ('comment', 'issue_comment', 'review_comment', 'commit_comment')
|
|
15
|
+
AND edge_type IN ('authored', 'commented_on', 'review_commented_on')
|
|
16
|
+
GROUP BY source_node_id, target_node_id
|
|
17
|
+
),
|
|
18
|
+
|
|
19
|
+
comment_mentions AS (
|
|
20
|
+
SELECT
|
|
21
|
+
source_node_id AS comment_node_id,
|
|
22
|
+
target_node_id AS mentioned_user_node_id,
|
|
23
|
+
min(occurred_at) AS first_seen_at,
|
|
24
|
+
max(occurred_at) AS last_seen_at
|
|
25
|
+
FROM {{ ref('hin_edges') }}
|
|
26
|
+
WHERE source_node_type = 'artifact'
|
|
27
|
+
AND source_node_subtype IN ('comment', 'issue_comment', 'review_comment', 'commit_comment')
|
|
28
|
+
AND target_node_type = 'user'
|
|
29
|
+
AND edge_type = 'mentions'
|
|
30
|
+
GROUP BY source_node_id, target_node_id
|
|
31
|
+
),
|
|
32
|
+
|
|
33
|
+
collapsed AS (
|
|
34
|
+
{{ collapse_two_hop_path(
|
|
35
|
+
first_relation='authored_comments',
|
|
36
|
+
second_relation='comment_mentions',
|
|
37
|
+
source_col='author_node_id',
|
|
38
|
+
mediator_col='comment_node_id',
|
|
39
|
+
second_source_col='comment_node_id',
|
|
40
|
+
target_col='mentioned_user_node_id',
|
|
41
|
+
directed=true
|
|
42
|
+
) }}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
{{ network_edges(
|
|
46
|
+
relation='collapsed',
|
|
47
|
+
recipe_name='user_mention_user',
|
|
48
|
+
source_node_id='source_node_id',
|
|
49
|
+
source_node_type="'user'",
|
|
50
|
+
target_node_id='target_node_id',
|
|
51
|
+
target_node_type="'user'",
|
|
52
|
+
edge_type="'mentions_user'",
|
|
53
|
+
directed='true',
|
|
54
|
+
weight='n_contexts',
|
|
55
|
+
weight_kind="'count'",
|
|
56
|
+
n_contexts='n_contexts',
|
|
57
|
+
first_seen_at='first_seen_at',
|
|
58
|
+
last_seen_at='last_seen_at',
|
|
59
|
+
properties="to_json({'comments': context_node_ids})"
|
|
60
|
+
) }}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{{ config(materialized='table') }}
|
|
2
|
+
|
|
3
|
+
{{ assert_recipe_supported('watch_user_repo') }}
|
|
4
|
+
|
|
5
|
+
WITH watch_edges AS (
|
|
6
|
+
{{ slice_edges(
|
|
7
|
+
edge_types=['watches'],
|
|
8
|
+
source_types=['user'],
|
|
9
|
+
target_types=['repo']
|
|
10
|
+
) }}
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
{{ network_edges(
|
|
14
|
+
relation='watch_edges',
|
|
15
|
+
recipe_name='watch_user_repo',
|
|
16
|
+
source_node_id='source_node_id',
|
|
17
|
+
source_node_type="'user'",
|
|
18
|
+
target_node_id='target_node_id',
|
|
19
|
+
target_node_type="'repo'",
|
|
20
|
+
edge_type="'watches'",
|
|
21
|
+
directed='true',
|
|
22
|
+
weight='coalesce(weight, 1.0)',
|
|
23
|
+
weight_kind="'binary'",
|
|
24
|
+
first_seen_at='occurred_at',
|
|
25
|
+
last_seen_at='occurred_at',
|
|
26
|
+
properties="to_json({'source_record_id': source_record_id})"
|
|
27
|
+
) }}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# dbt source declaration.
|
|
2
|
+
#
|
|
3
|
+
# DbtProjection creates dataset-scoped views before every dbt run.
|
|
4
|
+
#
|
|
5
|
+
# Core HIN projections should reference:
|
|
6
|
+
# {{ source('hin', 'active_hin_nodes') }}
|
|
7
|
+
# {{ source('hin', 'active_hin_edges') }}
|
|
8
|
+
#
|
|
9
|
+
# The legacy active_nodes / active_edges sources remain for old projections only.
|
|
10
|
+
# Never reference base tables directly — they contain all datasets.
|
|
11
|
+
version: 2
|
|
12
|
+
|
|
13
|
+
sources:
|
|
14
|
+
- name: hin
|
|
15
|
+
schema: main
|
|
16
|
+
tables:
|
|
17
|
+
- name: active_nodes
|
|
18
|
+
description: "View over nodes filtered to the active dataset_id."
|
|
19
|
+
columns:
|
|
20
|
+
- name: type
|
|
21
|
+
description: "NodeType — 'user', 'repo', 'artifact'."
|
|
22
|
+
- name: id
|
|
23
|
+
- name: ts
|
|
24
|
+
- name: attrs
|
|
25
|
+
- name: active_edges
|
|
26
|
+
description: "View over edges filtered to the active dataset_id."
|
|
27
|
+
columns:
|
|
28
|
+
- name: type
|
|
29
|
+
description: "EdgeType — 'opened', 'reviewed', etc."
|
|
30
|
+
- name: src_id
|
|
31
|
+
- name: dst_id
|
|
32
|
+
- name: ts
|
|
33
|
+
- name: attrs
|
|
34
|
+
- name: active_contract_accounts
|
|
35
|
+
description: "Adapter contract accounts for the active dataset_id."
|
|
36
|
+
- name: active_contract_repositories
|
|
37
|
+
description: "Adapter contract repositories for the active dataset_id."
|
|
38
|
+
- name: active_contract_artifacts
|
|
39
|
+
description: "Adapter contract artifacts for the active dataset_id."
|
|
40
|
+
- name: active_contract_relations
|
|
41
|
+
description: "Adapter contract relations for the active dataset_id."
|
|
42
|
+
- name: active_contract_adapter_manifest
|
|
43
|
+
description: "Adapter manifest/capabilities for the active dataset_id."
|
|
44
|
+
- name: active_hin_nodes
|
|
45
|
+
description: "Canonical HIN node view from HIN_DBT_DESIGN.md, filtered to the active dataset_id."
|
|
46
|
+
columns:
|
|
47
|
+
- name: node_id
|
|
48
|
+
- name: node_type
|
|
49
|
+
- name: node_subtype
|
|
50
|
+
- name: natural_key
|
|
51
|
+
- name: display_name
|
|
52
|
+
- name: properties
|
|
53
|
+
- name: active_hin_edges
|
|
54
|
+
description: "Canonical HIN edge view from HIN_DBT_DESIGN.md, filtered to the active dataset_id."
|
|
55
|
+
columns:
|
|
56
|
+
- name: edge_id
|
|
57
|
+
- name: source_node_id
|
|
58
|
+
- name: source_node_type
|
|
59
|
+
- name: target_node_id
|
|
60
|
+
- name: target_node_type
|
|
61
|
+
- name: edge_type
|
|
62
|
+
- name: relation_subtype
|
|
63
|
+
- name: properties
|
hinge/dbt/profiles.yml
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
recipe_name,required_capability
|
|
2
|
+
artifact_reference,has_artifact_refs
|
|
3
|
+
co_commit_user_user,has_commits
|
|
4
|
+
co_edit_file_user_user,has_commits
|
|
5
|
+
co_edit_file_user_user,has_file_touches
|
|
6
|
+
co_edit_line_user_user,has_commits
|
|
7
|
+
co_edit_line_user_user,has_line_touches
|
|
8
|
+
dev_interaction,has_pull_requests
|
|
9
|
+
dev_interaction,has_pr_reviews
|
|
10
|
+
dev_interaction,has_issues
|
|
11
|
+
dev_interaction,has_comments
|
|
12
|
+
developer_repo_affiliation,has_pull_requests
|
|
13
|
+
developer_repo_affiliation,has_pr_reviews
|
|
14
|
+
developer_repo_affiliation,has_issues
|
|
15
|
+
developer_repo_affiliation,has_comments
|
|
16
|
+
follow_user_user,has_follows
|
|
17
|
+
fork_repo_repo,has_forks
|
|
18
|
+
issue_co_participation,has_issues
|
|
19
|
+
issue_co_participation,has_comments
|
|
20
|
+
issue_participation,has_issues
|
|
21
|
+
issue_participation,has_comments
|
|
22
|
+
pr_author_reviewer,has_pull_requests
|
|
23
|
+
pr_author_reviewer,has_pr_reviews
|
|
24
|
+
pr_participation,has_pull_requests
|
|
25
|
+
pr_participation,has_pr_reviews
|
|
26
|
+
pr_reviewer_coreview,has_pull_requests
|
|
27
|
+
pr_reviewer_coreview,has_pr_reviews
|
|
28
|
+
repo_shared_contributors,has_pull_requests
|
|
29
|
+
repo_shared_contributors,has_pr_reviews
|
|
30
|
+
repo_shared_contributors,has_issues
|
|
31
|
+
repo_shared_contributors,has_comments
|
|
32
|
+
star_user_repo,has_stars
|
|
33
|
+
user_mention_user,has_mentions
|
|
34
|
+
user_mention_user,has_comments
|
|
35
|
+
watch_user_repo,has_watches
|
|
File without changes
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""Typer-based CLI entry point.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
|
|
5
|
+
hinge ingest path/to/events.jsonl --reader numfocus
|
|
6
|
+
hinge export --dataset <id> --projection dev-interaction --format gml -o out.gml
|
|
7
|
+
hinge list datasets
|
|
8
|
+
hinge list projections
|
|
9
|
+
hinge list readers
|
|
10
|
+
hinge list exporters
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import subprocess
|
|
16
|
+
from contextlib import suppress
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import typer
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
|
|
23
|
+
from hinge.config.logging_setup import setup_logging
|
|
24
|
+
from hinge.frontends import lib
|
|
25
|
+
|
|
26
|
+
setup_logging()
|
|
27
|
+
|
|
28
|
+
app = typer.Typer(help="hinge — Heterogeneous Information Network tool", no_args_is_help=True)
|
|
29
|
+
list_app = typer.Typer(help="List available resources", no_args_is_help=True)
|
|
30
|
+
app.add_typer(list_app, name="list")
|
|
31
|
+
|
|
32
|
+
console = Console()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command()
|
|
36
|
+
def ingest(
|
|
37
|
+
path: Path = typer.Argument(..., exists=True, readable=True, help="Dataset file."),
|
|
38
|
+
reader: str = typer.Option(..., "--reader", "-r", help="Reader name (e.g. numfocus)."),
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Read a dataset file, extract typed nodes/edges, persist to the DuckDB store."""
|
|
41
|
+
report = lib.ingest(path, reader=reader)
|
|
42
|
+
console.print(
|
|
43
|
+
f"[green]ingested[/green] {report.elements_read:,} elements "
|
|
44
|
+
f"→ {report.nodes_upserted:,} nodes, {report.edges_upserted:,} edges "
|
|
45
|
+
f"({report.violation_count} violations)\n"
|
|
46
|
+
f"[bold]Dataset ID:[/bold] {report.dataset_id}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@app.command()
|
|
51
|
+
def export(
|
|
52
|
+
dataset: str = typer.Option(..., "--dataset", "-d", help="Dataset ID from a previous ingest."),
|
|
53
|
+
projection: str = typer.Option(..., "--projection", "-p", help="Projection name."),
|
|
54
|
+
format_: str = typer.Option(..., "--format", "-f", help="Exporter name (e.g. gml)."),
|
|
55
|
+
output: Path = typer.Option(..., "--output", "-o", help="Output file."),
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Run a projection over a specific dataset and export the result."""
|
|
58
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
try:
|
|
60
|
+
with output.open("wb") as sink:
|
|
61
|
+
receipt = lib.export(
|
|
62
|
+
projection_name=projection, dataset_id=dataset, fmt=format_, sink=sink
|
|
63
|
+
)
|
|
64
|
+
except subprocess.CalledProcessError as exc:
|
|
65
|
+
_remove_empty_output(output)
|
|
66
|
+
_print_process_error(exc)
|
|
67
|
+
raise typer.Exit(code=1) from None
|
|
68
|
+
console.print(
|
|
69
|
+
f"[green]exported[/green] {receipt.node_count:,} nodes, "
|
|
70
|
+
f"{receipt.edge_count:,} edges → {output} "
|
|
71
|
+
f"(snapshot {receipt.snapshot_id[:12] if receipt.snapshot_id else '—'})"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@app.command("export-sql")
|
|
76
|
+
def export_sql(
|
|
77
|
+
sql: Path = typer.Argument(..., exists=True, readable=True, help="Custom dbt model SQL file."),
|
|
78
|
+
dataset: str = typer.Option(..., "--dataset", "-d", help="Dataset ID from a previous ingest."),
|
|
79
|
+
format_: str = typer.Option(..., "--format", "-f", help="Exporter name (e.g. gml)."),
|
|
80
|
+
output: Path = typer.Option(..., "--output", "-o", help="Output file."),
|
|
81
|
+
name: str | None = typer.Option(None, "--name", help="dbt model name; defaults to SQL stem."),
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Run a local SQL projection that can use the built-in dbt HIN macros."""
|
|
84
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
try:
|
|
86
|
+
with output.open("wb") as sink:
|
|
87
|
+
receipt = lib.export_sql_projection(
|
|
88
|
+
sql_path=sql, dataset_id=dataset, fmt=format_, sink=sink, name=name
|
|
89
|
+
)
|
|
90
|
+
except subprocess.CalledProcessError as exc:
|
|
91
|
+
_remove_empty_output(output)
|
|
92
|
+
_print_process_error(exc)
|
|
93
|
+
raise typer.Exit(code=1) from None
|
|
94
|
+
console.print(
|
|
95
|
+
f"[green]exported[/green] {receipt.node_count:,} nodes, "
|
|
96
|
+
f"{receipt.edge_count:,} edges → {output} "
|
|
97
|
+
f"(snapshot {receipt.snapshot_id[:12] if receipt.snapshot_id else '—'})"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _print_process_error(exc: subprocess.CalledProcessError) -> None:
|
|
102
|
+
console.print(f"[red]error:[/red] {_process_error_message(exc)}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _process_error_message(exc: subprocess.CalledProcessError) -> str:
|
|
106
|
+
text = f"{_process_text(exc.output)}\n{_process_text(exc.stderr)}"
|
|
107
|
+
capability_failure = _extract_capability_failure(text)
|
|
108
|
+
if capability_failure:
|
|
109
|
+
return capability_failure
|
|
110
|
+
return f"dbt failed with exit code {exc.returncode}. Set HINGE_LOG_LEVEL=DEBUG for details."
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _process_text(value: object) -> str:
|
|
114
|
+
if value is None:
|
|
115
|
+
return ""
|
|
116
|
+
if isinstance(value, bytes):
|
|
117
|
+
return value.decode(errors="replace")
|
|
118
|
+
if isinstance(value, str):
|
|
119
|
+
return value
|
|
120
|
+
return str(value)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _extract_capability_failure(text: str) -> str | None:
|
|
124
|
+
for line in text.splitlines():
|
|
125
|
+
if "Cannot build `" not in line or "Missing capabilities:" not in line:
|
|
126
|
+
continue
|
|
127
|
+
message = line.strip()
|
|
128
|
+
prefix_index = message.find("Cannot build `")
|
|
129
|
+
if prefix_index >= 0:
|
|
130
|
+
message = message[prefix_index:]
|
|
131
|
+
return message.replace(". Missing capabilities:", ".\nMissing capabilities:")
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _remove_empty_output(path: Path) -> None:
|
|
136
|
+
with suppress(OSError):
|
|
137
|
+
if path.exists() and path.stat().st_size == 0:
|
|
138
|
+
path.unlink()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@list_app.command("datasets")
|
|
142
|
+
def list_datasets() -> None:
|
|
143
|
+
"""Show all ingested datasets stored in the DuckDB file."""
|
|
144
|
+
datasets = lib.list_datasets()
|
|
145
|
+
if not datasets:
|
|
146
|
+
console.print("[yellow]No datasets ingested yet.[/yellow]")
|
|
147
|
+
return
|
|
148
|
+
table = Table("dataset_id", "reader", "path", "ingested_at", "nodes", "edges")
|
|
149
|
+
for d in datasets:
|
|
150
|
+
table.add_row(
|
|
151
|
+
d.dataset_id,
|
|
152
|
+
d.reader,
|
|
153
|
+
d.path,
|
|
154
|
+
d.ingested_at.strftime("%Y-%m-%d %H:%M:%S"),
|
|
155
|
+
str(d.node_count) if d.node_count is not None else "—",
|
|
156
|
+
str(d.edge_count) if d.edge_count is not None else "—",
|
|
157
|
+
)
|
|
158
|
+
console.print(table)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@list_app.command("projections")
|
|
162
|
+
def list_projections() -> None:
|
|
163
|
+
"""List all registered projection names (pass to --projection on export)."""
|
|
164
|
+
table = Table("name")
|
|
165
|
+
for name in lib.list_projections():
|
|
166
|
+
table.add_row(name)
|
|
167
|
+
console.print(table)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@list_app.command("exporters")
|
|
171
|
+
def list_exporters() -> None:
|
|
172
|
+
"""List all registered exporter names (pass to --format on export)."""
|
|
173
|
+
table = Table("name")
|
|
174
|
+
for name in lib.list_exporters():
|
|
175
|
+
table.add_row(name)
|
|
176
|
+
console.print(table)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@list_app.command("readers")
|
|
180
|
+
def list_readers() -> None:
|
|
181
|
+
"""List all registered reader names (pass to --reader on ingest)."""
|
|
182
|
+
table = Table("name")
|
|
183
|
+
for name in lib.list_readers():
|
|
184
|
+
table.add_row(name)
|
|
185
|
+
console.print(table)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
if __name__ == "__main__":
|
|
189
|
+
app()
|
hinge/frontends/lib.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Public library facade.
|
|
2
|
+
|
|
3
|
+
``import hinge`` and you get these functions. Each one assembles the pipeline
|
|
4
|
+
through the registry and hands it to the runner — frontends do not import
|
|
5
|
+
concrete stage classes.
|
|
6
|
+
|
|
7
|
+
The three-phase export dance (scope → dbt → fingerprint) is explicit here so
|
|
8
|
+
the DuckDB single-writer lock is released between phases. The store is a
|
|
9
|
+
context manager; the surrounding ``with`` blocks make ownership obvious.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, BinaryIO, cast
|
|
16
|
+
|
|
17
|
+
from hinge.config.settings import types_yaml_path
|
|
18
|
+
from hinge.kernel import registry, runner
|
|
19
|
+
from hinge.kernel.projection.projection_spec import ProjectionSpec
|
|
20
|
+
from hinge.kernel.protocols.store_stage import DatasetMeta
|
|
21
|
+
from hinge.kernel.runner import IngestReport
|
|
22
|
+
from hinge.kernel.schema.hin_schema import HINSchema
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def ingest(path: str | Path, reader: str) -> IngestReport:
|
|
26
|
+
rdr = registry.get_reader(reader, path=path)
|
|
27
|
+
schema = HINSchema.from_yaml(types_yaml_path())
|
|
28
|
+
with registry.get_store() as store:
|
|
29
|
+
return runner.run_ingest(rdr, store, schema)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def export(
|
|
33
|
+
projection_name: str,
|
|
34
|
+
dataset_id: str,
|
|
35
|
+
fmt: str,
|
|
36
|
+
sink: BinaryIO,
|
|
37
|
+
**params: Any,
|
|
38
|
+
) -> Any:
|
|
39
|
+
schema = HINSchema.from_yaml(types_yaml_path())
|
|
40
|
+
proj_stage = registry.get_projection("dbt")
|
|
41
|
+
spec = registry.get_projection_spec(projection_name)
|
|
42
|
+
exporter = registry.get_exporter(fmt)
|
|
43
|
+
|
|
44
|
+
# Phase 1: writable store creates dataset-scoped views, then releases the
|
|
45
|
+
# file lock so the dbt subprocess can acquire it.
|
|
46
|
+
with registry.get_store() as store:
|
|
47
|
+
view = store.scope_to_dataset(dataset_id)
|
|
48
|
+
|
|
49
|
+
# Phase 2: dbt subprocess materialises the projection.
|
|
50
|
+
handle = runner.run_projection(proj_stage, spec, view, params)
|
|
51
|
+
|
|
52
|
+
# Phase 3: read-only store for fingerprinting.
|
|
53
|
+
with registry.get_store(read_only=True) as store:
|
|
54
|
+
return runner.run_export(
|
|
55
|
+
handle,
|
|
56
|
+
exporter,
|
|
57
|
+
sink,
|
|
58
|
+
store=store,
|
|
59
|
+
projection=proj_stage,
|
|
60
|
+
schema=schema,
|
|
61
|
+
dataset_id=dataset_id,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def export_sql_projection(
|
|
66
|
+
sql_path: str | Path,
|
|
67
|
+
dataset_id: str,
|
|
68
|
+
fmt: str,
|
|
69
|
+
sink: BinaryIO,
|
|
70
|
+
*,
|
|
71
|
+
name: str | None = None,
|
|
72
|
+
**params: Any,
|
|
73
|
+
) -> Any:
|
|
74
|
+
sql_path = Path(sql_path)
|
|
75
|
+
model_name = name or sql_path.stem
|
|
76
|
+
schema = HINSchema.from_yaml(types_yaml_path())
|
|
77
|
+
proj_stage = registry.get_projection(
|
|
78
|
+
"dbt", custom_model_path=sql_path, custom_model_name=model_name
|
|
79
|
+
)
|
|
80
|
+
spec = ProjectionSpec(
|
|
81
|
+
name=model_name,
|
|
82
|
+
description=f"Custom SQL projection from {sql_path}",
|
|
83
|
+
model_name=model_name,
|
|
84
|
+
output_node_types=[],
|
|
85
|
+
output_edge_types=[],
|
|
86
|
+
)
|
|
87
|
+
exporter = registry.get_exporter(fmt)
|
|
88
|
+
|
|
89
|
+
with registry.get_store() as store:
|
|
90
|
+
view = store.scope_to_dataset(dataset_id)
|
|
91
|
+
|
|
92
|
+
handle = runner.run_projection(proj_stage, spec, view, params)
|
|
93
|
+
|
|
94
|
+
with registry.get_store(read_only=True) as store:
|
|
95
|
+
return runner.run_export(
|
|
96
|
+
handle,
|
|
97
|
+
exporter,
|
|
98
|
+
sink,
|
|
99
|
+
store=store,
|
|
100
|
+
projection=proj_stage,
|
|
101
|
+
schema=schema,
|
|
102
|
+
dataset_id=dataset_id,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def project(name: str, dataset_id: str, **params: Any) -> Any:
|
|
107
|
+
proj_stage = registry.get_projection("dbt")
|
|
108
|
+
spec = registry.get_projection_spec(name)
|
|
109
|
+
with registry.get_store() as store:
|
|
110
|
+
view = store.scope_to_dataset(dataset_id)
|
|
111
|
+
return runner.run_projection(proj_stage, spec, view, params)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def project_sql(
|
|
115
|
+
sql_path: str | Path, dataset_id: str, *, name: str | None = None, **params: Any
|
|
116
|
+
) -> Any:
|
|
117
|
+
sql_path = Path(sql_path)
|
|
118
|
+
model_name = name or sql_path.stem
|
|
119
|
+
proj_stage = registry.get_projection(
|
|
120
|
+
"dbt", custom_model_path=sql_path, custom_model_name=model_name
|
|
121
|
+
)
|
|
122
|
+
spec = ProjectionSpec(
|
|
123
|
+
name=model_name,
|
|
124
|
+
description=f"Custom SQL projection from {sql_path}",
|
|
125
|
+
model_name=model_name,
|
|
126
|
+
output_node_types=[],
|
|
127
|
+
output_edge_types=[],
|
|
128
|
+
)
|
|
129
|
+
with registry.get_store() as store:
|
|
130
|
+
view = store.scope_to_dataset(dataset_id)
|
|
131
|
+
return runner.run_projection(proj_stage, spec, view, params)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def list_datasets() -> list[DatasetMeta]:
|
|
135
|
+
with registry.get_store(read_only=True) as store:
|
|
136
|
+
return cast(list[DatasetMeta], store.list_datasets())
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def list_projections() -> list[str]:
|
|
140
|
+
return registry.list_registered("projection_spec")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def list_exporters() -> list[str]:
|
|
144
|
+
return registry.list_registered("exporter")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def list_readers() -> list[str]:
|
|
148
|
+
return registry.list_registered("reader")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Textual TUI — stub.
|
|
2
|
+
|
|
3
|
+
Plan: a single-screen app that lists registered sources, projections and
|
|
4
|
+
exporters; lets the user pick one of each; runs the pipeline; streams progress.
|
|
5
|
+
The frontend must compose stages via registry + runner only — no direct
|
|
6
|
+
imports from hinge.stages.
|
|
7
|
+
"""
|
hinge/kernel/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from hinge.kernel.projection.projected_graph import ProjectedGraph, ProjectedGraphHandle
|
|
2
|
+
from hinge.kernel.projection.projection_spec import ProjectionParams, ProjectionSpec
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"ProjectedGraph",
|
|
6
|
+
"ProjectedGraphHandle",
|
|
7
|
+
"ProjectionParams",
|
|
8
|
+
"ProjectionSpec",
|
|
9
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from typing import Protocol
|
|
5
|
+
|
|
6
|
+
from hinge.kernel.schema.typed_edge import TypedEdge
|
|
7
|
+
from hinge.kernel.schema.typed_node import TypedNode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProjectedGraphHandle(Protocol):
|
|
11
|
+
"""Lazy cursor over a materialised projection.
|
|
12
|
+
|
|
13
|
+
Implementations stream rows from the result table — exporters must not
|
|
14
|
+
assume the full graph fits in memory.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def iter_nodes(self) -> Iterator[TypedNode]: ...
|
|
18
|
+
def iter_edges(self) -> Iterator[TypedEdge]: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProjectedGraph:
|
|
22
|
+
"""Concrete, in-memory ProjectedGraphHandle. Useful for tests."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, nodes: list[TypedNode], edges: list[TypedEdge]) -> None:
|
|
25
|
+
self._nodes = nodes
|
|
26
|
+
self._edges = edges
|
|
27
|
+
|
|
28
|
+
def iter_nodes(self) -> Iterator[TypedNode]:
|
|
29
|
+
return iter(self._nodes)
|
|
30
|
+
|
|
31
|
+
def iter_edges(self) -> Iterator[TypedEdge]:
|
|
32
|
+
return iter(self._edges)
|