rdf-construct 0.2.1__py3-none-any.whl → 0.4.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.
- rdf_construct/__init__.py +1 -1
- rdf_construct/cli.py +1794 -0
- rdf_construct/describe/__init__.py +93 -0
- rdf_construct/describe/analyzer.py +176 -0
- rdf_construct/describe/documentation.py +146 -0
- rdf_construct/describe/formatters/__init__.py +47 -0
- rdf_construct/describe/formatters/json.py +65 -0
- rdf_construct/describe/formatters/markdown.py +275 -0
- rdf_construct/describe/formatters/text.py +315 -0
- rdf_construct/describe/hierarchy.py +232 -0
- rdf_construct/describe/imports.py +213 -0
- rdf_construct/describe/metadata.py +187 -0
- rdf_construct/describe/metrics.py +145 -0
- rdf_construct/describe/models.py +552 -0
- rdf_construct/describe/namespaces.py +180 -0
- rdf_construct/describe/profiles.py +415 -0
- rdf_construct/localise/__init__.py +114 -0
- rdf_construct/localise/config.py +508 -0
- rdf_construct/localise/extractor.py +427 -0
- rdf_construct/localise/formatters/__init__.py +36 -0
- rdf_construct/localise/formatters/markdown.py +229 -0
- rdf_construct/localise/formatters/text.py +224 -0
- rdf_construct/localise/merger.py +346 -0
- rdf_construct/localise/reporter.py +356 -0
- rdf_construct/merge/__init__.py +165 -0
- rdf_construct/merge/config.py +354 -0
- rdf_construct/merge/conflicts.py +281 -0
- rdf_construct/merge/formatters.py +426 -0
- rdf_construct/merge/merger.py +425 -0
- rdf_construct/merge/migrator.py +339 -0
- rdf_construct/merge/rules.py +377 -0
- rdf_construct/merge/splitter.py +1102 -0
- rdf_construct/refactor/__init__.py +72 -0
- rdf_construct/refactor/config.py +362 -0
- rdf_construct/refactor/deprecator.py +328 -0
- rdf_construct/refactor/formatters/__init__.py +8 -0
- rdf_construct/refactor/formatters/text.py +311 -0
- rdf_construct/refactor/renamer.py +294 -0
- {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/METADATA +91 -6
- {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/RECORD +43 -7
- {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/WHEEL +0 -0
- {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/entry_points.txt +0 -0
- {rdf_construct-0.2.1.dist-info → rdf_construct-0.4.0.dist-info}/licenses/LICENSE +0 -0
rdf_construct/cli.py
CHANGED
|
@@ -62,6 +62,58 @@ from rdf_construct.stats import (
|
|
|
62
62
|
format_comparison,
|
|
63
63
|
)
|
|
64
64
|
|
|
65
|
+
from rdf_construct.merge import (
|
|
66
|
+
MergeConfig,
|
|
67
|
+
SourceConfig,
|
|
68
|
+
OutputConfig,
|
|
69
|
+
ConflictConfig,
|
|
70
|
+
ConflictStrategy,
|
|
71
|
+
ImportsStrategy,
|
|
72
|
+
DataMigrationConfig,
|
|
73
|
+
OntologyMerger,
|
|
74
|
+
load_merge_config,
|
|
75
|
+
create_default_config,
|
|
76
|
+
get_formatter,
|
|
77
|
+
migrate_data_files,
|
|
78
|
+
# Split imports
|
|
79
|
+
OntologySplitter,
|
|
80
|
+
SplitConfig,
|
|
81
|
+
SplitResult,
|
|
82
|
+
ModuleDefinition,
|
|
83
|
+
split_by_namespace,
|
|
84
|
+
create_default_split_config,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
from rdf_construct.refactor import (
|
|
88
|
+
RenameConfig,
|
|
89
|
+
DeprecationSpec,
|
|
90
|
+
RefactorConfig,
|
|
91
|
+
OntologyRenamer,
|
|
92
|
+
OntologyDeprecator,
|
|
93
|
+
TextFormatter as RefactorTextFormatter,
|
|
94
|
+
load_refactor_config,
|
|
95
|
+
create_default_rename_config,
|
|
96
|
+
create_default_deprecation_config,
|
|
97
|
+
rename_file,
|
|
98
|
+
rename_files,
|
|
99
|
+
deprecate_file,
|
|
100
|
+
)
|
|
101
|
+
from rdf_construct.merge import DataMigrator
|
|
102
|
+
|
|
103
|
+
from rdf_construct.localise import (
|
|
104
|
+
StringExtractor,
|
|
105
|
+
TranslationMerger,
|
|
106
|
+
CoverageReporter,
|
|
107
|
+
ExtractConfig,
|
|
108
|
+
MergeConfig as LocaliseMergeConfig,
|
|
109
|
+
TranslationFile,
|
|
110
|
+
TranslationStatus,
|
|
111
|
+
ExistingStrategy,
|
|
112
|
+
create_default_config as create_default_localise_config,
|
|
113
|
+
load_localise_config,
|
|
114
|
+
get_formatter as get_localise_formatter,
|
|
115
|
+
)
|
|
116
|
+
|
|
65
117
|
# Valid rendering modes
|
|
66
118
|
RENDERING_MODES = ["default", "odm"]
|
|
67
119
|
|
|
@@ -1758,5 +1810,1747 @@ def stats(
|
|
|
1758
1810
|
sys.exit(1)
|
|
1759
1811
|
|
|
1760
1812
|
|
|
1813
|
+
@cli.command()
|
|
1814
|
+
@click.argument("file", type=click.Path(exists=True, path_type=Path))
|
|
1815
|
+
@click.option(
|
|
1816
|
+
"--output",
|
|
1817
|
+
"-o",
|
|
1818
|
+
type=click.Path(path_type=Path),
|
|
1819
|
+
help="Write output to file instead of stdout",
|
|
1820
|
+
)
|
|
1821
|
+
@click.option(
|
|
1822
|
+
"--format",
|
|
1823
|
+
"-f",
|
|
1824
|
+
"output_format",
|
|
1825
|
+
type=click.Choice(["text", "json", "markdown", "md"], case_sensitive=False),
|
|
1826
|
+
default="text",
|
|
1827
|
+
help="Output format (default: text)",
|
|
1828
|
+
)
|
|
1829
|
+
@click.option(
|
|
1830
|
+
"--brief",
|
|
1831
|
+
is_flag=True,
|
|
1832
|
+
help="Show brief summary only (metadata, metrics, profile)",
|
|
1833
|
+
)
|
|
1834
|
+
@click.option(
|
|
1835
|
+
"--no-resolve",
|
|
1836
|
+
is_flag=True,
|
|
1837
|
+
help="Skip import resolution checks",
|
|
1838
|
+
)
|
|
1839
|
+
@click.option(
|
|
1840
|
+
"--reasoning",
|
|
1841
|
+
is_flag=True,
|
|
1842
|
+
help="Include reasoning analysis",
|
|
1843
|
+
)
|
|
1844
|
+
@click.option(
|
|
1845
|
+
"--no-colour",
|
|
1846
|
+
"--no-color",
|
|
1847
|
+
is_flag=True,
|
|
1848
|
+
help="Disable coloured output (text format only)",
|
|
1849
|
+
)
|
|
1850
|
+
def describe(
|
|
1851
|
+
file: Path,
|
|
1852
|
+
output: Path | None,
|
|
1853
|
+
output_format: str,
|
|
1854
|
+
brief: bool,
|
|
1855
|
+
no_resolve: bool,
|
|
1856
|
+
reasoning: bool,
|
|
1857
|
+
no_colour: bool,
|
|
1858
|
+
):
|
|
1859
|
+
"""Describe an ontology: profile, metrics, imports, and structure.
|
|
1860
|
+
|
|
1861
|
+
Provides a comprehensive analysis of an RDF ontology file, including:
|
|
1862
|
+
- Profile detection (RDF, RDFS, OWL DL, OWL Full)
|
|
1863
|
+
- Basic metrics (classes, properties, individuals)
|
|
1864
|
+
- Import analysis with optional resolvability checking
|
|
1865
|
+
- Namespace categorisation
|
|
1866
|
+
- Class hierarchy analysis
|
|
1867
|
+
- Documentation coverage
|
|
1868
|
+
|
|
1869
|
+
FILE: RDF ontology file to describe (.ttl, .rdf, .owl, etc.)
|
|
1870
|
+
|
|
1871
|
+
\b
|
|
1872
|
+
Examples:
|
|
1873
|
+
# Basic description
|
|
1874
|
+
rdf-construct describe ontology.ttl
|
|
1875
|
+
|
|
1876
|
+
# Brief summary only
|
|
1877
|
+
rdf-construct describe ontology.ttl --brief
|
|
1878
|
+
|
|
1879
|
+
# JSON output for programmatic use
|
|
1880
|
+
rdf-construct describe ontology.ttl --format json -o description.json
|
|
1881
|
+
|
|
1882
|
+
# Markdown for documentation
|
|
1883
|
+
rdf-construct describe ontology.ttl --format markdown -o DESCRIPTION.md
|
|
1884
|
+
|
|
1885
|
+
# Skip slow import resolution
|
|
1886
|
+
rdf-construct describe ontology.ttl --no-resolve
|
|
1887
|
+
|
|
1888
|
+
\b
|
|
1889
|
+
Exit codes:
|
|
1890
|
+
0 - Success
|
|
1891
|
+
1 - Success with warnings (unresolvable imports, etc.)
|
|
1892
|
+
2 - Error (file not found, parse error)
|
|
1893
|
+
"""
|
|
1894
|
+
from rdf_construct.describe import describe_file, format_description
|
|
1895
|
+
|
|
1896
|
+
try:
|
|
1897
|
+
click.echo(f"Analysing {file}...", err=True)
|
|
1898
|
+
|
|
1899
|
+
# Perform analysis
|
|
1900
|
+
description = describe_file(
|
|
1901
|
+
file,
|
|
1902
|
+
brief=brief,
|
|
1903
|
+
resolve_imports=not no_resolve,
|
|
1904
|
+
include_reasoning=reasoning,
|
|
1905
|
+
)
|
|
1906
|
+
|
|
1907
|
+
# Format output
|
|
1908
|
+
use_colour = not no_colour and output_format == "text" and output is None
|
|
1909
|
+
formatted = format_description(
|
|
1910
|
+
description,
|
|
1911
|
+
format_name=output_format,
|
|
1912
|
+
use_colour=use_colour,
|
|
1913
|
+
)
|
|
1914
|
+
|
|
1915
|
+
# Write output
|
|
1916
|
+
if output:
|
|
1917
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
1918
|
+
output.write_text(formatted)
|
|
1919
|
+
click.secho(f"✓ Wrote description to {output}", fg="green", err=True)
|
|
1920
|
+
else:
|
|
1921
|
+
click.echo(formatted)
|
|
1922
|
+
|
|
1923
|
+
# Exit code based on warnings
|
|
1924
|
+
if description.imports and description.imports.unresolvable_count > 0:
|
|
1925
|
+
sys.exit(1)
|
|
1926
|
+
else:
|
|
1927
|
+
sys.exit(0)
|
|
1928
|
+
|
|
1929
|
+
except FileNotFoundError as e:
|
|
1930
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
1931
|
+
sys.exit(2)
|
|
1932
|
+
except ValueError as e:
|
|
1933
|
+
click.secho(f"Error parsing RDF: {e}", fg="red", err=True)
|
|
1934
|
+
sys.exit(2)
|
|
1935
|
+
except Exception as e:
|
|
1936
|
+
click.secho(f"Error: {e}", fg="red", err=True)
|
|
1937
|
+
sys.exit(2)
|
|
1938
|
+
|
|
1939
|
+
|
|
1940
|
+
@cli.command()
|
|
1941
|
+
@click.argument("sources", nargs=-1, type=click.Path(exists=True, path_type=Path))
|
|
1942
|
+
@click.option(
|
|
1943
|
+
"--output",
|
|
1944
|
+
"-o",
|
|
1945
|
+
type=click.Path(path_type=Path),
|
|
1946
|
+
required=True,
|
|
1947
|
+
help="Output file for merged ontology",
|
|
1948
|
+
)
|
|
1949
|
+
@click.option(
|
|
1950
|
+
"--config",
|
|
1951
|
+
"-c",
|
|
1952
|
+
"config_file",
|
|
1953
|
+
type=click.Path(exists=True, path_type=Path),
|
|
1954
|
+
help="YAML configuration file",
|
|
1955
|
+
)
|
|
1956
|
+
@click.option(
|
|
1957
|
+
"--priority",
|
|
1958
|
+
"-p",
|
|
1959
|
+
multiple=True,
|
|
1960
|
+
type=int,
|
|
1961
|
+
help="Priority for each source (order matches sources)",
|
|
1962
|
+
)
|
|
1963
|
+
@click.option(
|
|
1964
|
+
"--strategy",
|
|
1965
|
+
type=click.Choice(["priority", "first", "last", "mark_all"], case_sensitive=False),
|
|
1966
|
+
default="priority",
|
|
1967
|
+
help="Conflict resolution strategy (default: priority)",
|
|
1968
|
+
)
|
|
1969
|
+
@click.option(
|
|
1970
|
+
"--report",
|
|
1971
|
+
"-r",
|
|
1972
|
+
type=click.Path(path_type=Path),
|
|
1973
|
+
help="Write conflict report to file",
|
|
1974
|
+
)
|
|
1975
|
+
@click.option(
|
|
1976
|
+
"--report-format",
|
|
1977
|
+
type=click.Choice(["text", "markdown", "md"], case_sensitive=False),
|
|
1978
|
+
default="markdown",
|
|
1979
|
+
help="Format for conflict report (default: markdown)",
|
|
1980
|
+
)
|
|
1981
|
+
@click.option(
|
|
1982
|
+
"--imports",
|
|
1983
|
+
type=click.Choice(["preserve", "remove", "merge"], case_sensitive=False),
|
|
1984
|
+
default="preserve",
|
|
1985
|
+
help="How to handle owl:imports (default: preserve)",
|
|
1986
|
+
)
|
|
1987
|
+
@click.option(
|
|
1988
|
+
"--migrate-data",
|
|
1989
|
+
multiple=True,
|
|
1990
|
+
type=click.Path(exists=True, path_type=Path),
|
|
1991
|
+
help="Data file(s) to migrate",
|
|
1992
|
+
)
|
|
1993
|
+
@click.option(
|
|
1994
|
+
"--migration-rules",
|
|
1995
|
+
type=click.Path(exists=True, path_type=Path),
|
|
1996
|
+
help="YAML file with migration rules",
|
|
1997
|
+
)
|
|
1998
|
+
@click.option(
|
|
1999
|
+
"--data-output",
|
|
2000
|
+
type=click.Path(path_type=Path),
|
|
2001
|
+
help="Output path for migrated data",
|
|
2002
|
+
)
|
|
2003
|
+
@click.option(
|
|
2004
|
+
"--dry-run",
|
|
2005
|
+
is_flag=True,
|
|
2006
|
+
help="Show what would happen without writing files",
|
|
2007
|
+
)
|
|
2008
|
+
@click.option(
|
|
2009
|
+
"--no-colour",
|
|
2010
|
+
is_flag=True,
|
|
2011
|
+
help="Disable coloured output",
|
|
2012
|
+
)
|
|
2013
|
+
@click.option(
|
|
2014
|
+
"--init",
|
|
2015
|
+
"init_config",
|
|
2016
|
+
is_flag=True,
|
|
2017
|
+
help="Generate a default merge configuration file",
|
|
2018
|
+
)
|
|
2019
|
+
def merge(
|
|
2020
|
+
sources: tuple[Path, ...],
|
|
2021
|
+
output: Path,
|
|
2022
|
+
config_file: Path | None,
|
|
2023
|
+
priority: tuple[int, ...],
|
|
2024
|
+
strategy: str,
|
|
2025
|
+
report: Path | None,
|
|
2026
|
+
report_format: str,
|
|
2027
|
+
imports: str,
|
|
2028
|
+
migrate_data: tuple[Path, ...],
|
|
2029
|
+
migration_rules: Path | None,
|
|
2030
|
+
data_output: Path | None,
|
|
2031
|
+
dry_run: bool,
|
|
2032
|
+
no_colour: bool,
|
|
2033
|
+
init_config: bool,
|
|
2034
|
+
):
|
|
2035
|
+
"""Merge multiple RDF ontology files.
|
|
2036
|
+
|
|
2037
|
+
Combines SOURCES into a single output ontology, detecting and handling
|
|
2038
|
+
conflicts between definitions.
|
|
2039
|
+
|
|
2040
|
+
\b
|
|
2041
|
+
SOURCES: One or more RDF files to merge (.ttl, .rdf, .owl)
|
|
2042
|
+
|
|
2043
|
+
\b
|
|
2044
|
+
Exit codes:
|
|
2045
|
+
0 - Merge successful, no unresolved conflicts
|
|
2046
|
+
1 - Merge successful, but unresolved conflicts marked in output
|
|
2047
|
+
2 - Error (file not found, parse error, etc.)
|
|
2048
|
+
|
|
2049
|
+
\b
|
|
2050
|
+
Examples:
|
|
2051
|
+
# Basic merge of two files
|
|
2052
|
+
rdf-construct merge core.ttl ext.ttl -o merged.ttl
|
|
2053
|
+
|
|
2054
|
+
# With priorities (higher wins conflicts)
|
|
2055
|
+
rdf-construct merge core.ttl ext.ttl -o merged.ttl -p 1 -p 2
|
|
2056
|
+
|
|
2057
|
+
# Generate conflict report
|
|
2058
|
+
rdf-construct merge core.ttl ext.ttl -o merged.ttl --report conflicts.md
|
|
2059
|
+
|
|
2060
|
+
# Mark all conflicts for manual review
|
|
2061
|
+
rdf-construct merge core.ttl ext.ttl -o merged.ttl --strategy mark_all
|
|
2062
|
+
|
|
2063
|
+
# With data migration
|
|
2064
|
+
rdf-construct merge core.ttl ext.ttl -o merged.ttl \\
|
|
2065
|
+
--migrate-data split_instances.ttl --data-output migrated.ttl
|
|
2066
|
+
|
|
2067
|
+
# Use configuration file
|
|
2068
|
+
rdf-construct merge --config merge.yml -o merged.ttl
|
|
2069
|
+
|
|
2070
|
+
# Generate default config file
|
|
2071
|
+
rdf-construct merge --init
|
|
2072
|
+
"""
|
|
2073
|
+
# Handle --init flag
|
|
2074
|
+
if init_config:
|
|
2075
|
+
config_path = Path("merge.yml")
|
|
2076
|
+
if config_path.exists():
|
|
2077
|
+
click.secho(f"Config file already exists: {config_path}", fg="red", err=True)
|
|
2078
|
+
raise click.Abort()
|
|
2079
|
+
|
|
2080
|
+
config_content = create_default_config()
|
|
2081
|
+
config_path.write_text(config_content)
|
|
2082
|
+
click.secho(f"Created {config_path}", fg="green")
|
|
2083
|
+
click.echo("Edit this file to configure your merge, then run:")
|
|
2084
|
+
click.echo(f" rdf-construct merge --config {config_path} -o merged.ttl")
|
|
2085
|
+
return
|
|
2086
|
+
|
|
2087
|
+
# Validate we have sources
|
|
2088
|
+
if not sources and not config_file:
|
|
2089
|
+
click.secho("Error: No source files specified.", fg="red", err=True)
|
|
2090
|
+
click.echo("Provide source files or use --config with a configuration file.", err=True)
|
|
2091
|
+
raise click.Abort()
|
|
2092
|
+
|
|
2093
|
+
# Build configuration
|
|
2094
|
+
if config_file:
|
|
2095
|
+
try:
|
|
2096
|
+
config = load_merge_config(config_file)
|
|
2097
|
+
click.echo(f"Using config: {config_file}")
|
|
2098
|
+
|
|
2099
|
+
# Override output if provided on CLI
|
|
2100
|
+
if output:
|
|
2101
|
+
config.output = OutputConfig(path=output)
|
|
2102
|
+
except (FileNotFoundError, ValueError) as e:
|
|
2103
|
+
click.secho(f"Error loading config: {e}", fg="red", err=True)
|
|
2104
|
+
raise click.Abort()
|
|
2105
|
+
else:
|
|
2106
|
+
# Build config from CLI arguments
|
|
2107
|
+
priorities_list = list(priority) if priority else list(range(1, len(sources) + 1))
|
|
2108
|
+
|
|
2109
|
+
# Pad priorities if needed
|
|
2110
|
+
while len(priorities_list) < len(sources):
|
|
2111
|
+
priorities_list.append(len(priorities_list) + 1)
|
|
2112
|
+
|
|
2113
|
+
source_configs = [
|
|
2114
|
+
SourceConfig(path=p, priority=pri)
|
|
2115
|
+
for p, pri in zip(sources, priorities_list)
|
|
2116
|
+
]
|
|
2117
|
+
|
|
2118
|
+
conflict_strategy = ConflictStrategy[strategy.upper()]
|
|
2119
|
+
imports_strategy = ImportsStrategy[imports.upper()]
|
|
2120
|
+
|
|
2121
|
+
# Data migration config
|
|
2122
|
+
data_migration = None
|
|
2123
|
+
if migrate_data:
|
|
2124
|
+
data_migration = DataMigrationConfig(
|
|
2125
|
+
data_sources=list(migrate_data),
|
|
2126
|
+
output_path=data_output,
|
|
2127
|
+
)
|
|
2128
|
+
|
|
2129
|
+
config = MergeConfig(
|
|
2130
|
+
sources=source_configs,
|
|
2131
|
+
output=OutputConfig(path=output),
|
|
2132
|
+
conflicts=ConflictConfig(
|
|
2133
|
+
strategy=conflict_strategy,
|
|
2134
|
+
report_path=report,
|
|
2135
|
+
),
|
|
2136
|
+
imports=imports_strategy,
|
|
2137
|
+
migrate_data=data_migration,
|
|
2138
|
+
dry_run=dry_run,
|
|
2139
|
+
)
|
|
2140
|
+
|
|
2141
|
+
# Execute merge
|
|
2142
|
+
click.echo("Merging ontologies...")
|
|
2143
|
+
|
|
2144
|
+
merger = OntologyMerger(config)
|
|
2145
|
+
result = merger.merge()
|
|
2146
|
+
|
|
2147
|
+
if not result.success:
|
|
2148
|
+
click.secho(f"✗ Merge failed: {result.error}", fg="red", err=True)
|
|
2149
|
+
raise SystemExit(2)
|
|
2150
|
+
|
|
2151
|
+
# Display results
|
|
2152
|
+
use_colour = not no_colour
|
|
2153
|
+
text_formatter = get_formatter("text", use_colour=use_colour)
|
|
2154
|
+
click.echo(text_formatter.format_merge_result(result, result.merged_graph))
|
|
2155
|
+
|
|
2156
|
+
# Write output (unless dry run)
|
|
2157
|
+
if not dry_run and result.merged_graph and config.output:
|
|
2158
|
+
merger.write_output(result, config.output.path)
|
|
2159
|
+
click.echo()
|
|
2160
|
+
click.secho(f"✓ Wrote {config.output.path}", fg="green")
|
|
2161
|
+
|
|
2162
|
+
# Generate conflict report if requested
|
|
2163
|
+
if report and result.conflicts:
|
|
2164
|
+
report_formatter = get_formatter(report_format)
|
|
2165
|
+
report_content = report_formatter.format_conflict_report(
|
|
2166
|
+
result.conflicts, result.merged_graph
|
|
2167
|
+
)
|
|
2168
|
+
report.parent.mkdir(parents=True, exist_ok=True)
|
|
2169
|
+
report.write_text(report_content)
|
|
2170
|
+
click.echo(f" Conflict report: {report}")
|
|
2171
|
+
|
|
2172
|
+
# Handle data migration
|
|
2173
|
+
if config.migrate_data and config.migrate_data.data_sources:
|
|
2174
|
+
click.echo()
|
|
2175
|
+
click.echo("Migrating data...")
|
|
2176
|
+
|
|
2177
|
+
# Build URI map from any namespace remappings
|
|
2178
|
+
from rdf_construct.merge import DataMigrator
|
|
2179
|
+
|
|
2180
|
+
migrator = DataMigrator()
|
|
2181
|
+
uri_map: dict[URIRef, URIRef] = {}
|
|
2182
|
+
|
|
2183
|
+
# Collect namespace remaps from all sources
|
|
2184
|
+
for src in config.sources:
|
|
2185
|
+
if src.namespace_remap:
|
|
2186
|
+
for old_ns, new_ns in src.namespace_remap.items():
|
|
2187
|
+
# We'd need to scan data files to build complete map
|
|
2188
|
+
# For now, this is a placeholder
|
|
2189
|
+
pass
|
|
2190
|
+
|
|
2191
|
+
# Apply migration
|
|
2192
|
+
migration_result = migrate_data_files(
|
|
2193
|
+
data_paths=config.migrate_data.data_sources,
|
|
2194
|
+
uri_map=uri_map if uri_map else None,
|
|
2195
|
+
rules=config.migrate_data.rules if config.migrate_data.rules else None,
|
|
2196
|
+
output_path=config.migrate_data.output_path if not dry_run else None,
|
|
2197
|
+
)
|
|
2198
|
+
|
|
2199
|
+
if migration_result.success:
|
|
2200
|
+
click.echo(text_formatter.format_migration_result(migration_result))
|
|
2201
|
+
if config.migrate_data.output_path and not dry_run:
|
|
2202
|
+
click.secho(
|
|
2203
|
+
f"✓ Wrote migrated data to {config.migrate_data.output_path}",
|
|
2204
|
+
fg="green",
|
|
2205
|
+
)
|
|
2206
|
+
else:
|
|
2207
|
+
click.secho(
|
|
2208
|
+
f"✗ Data migration failed: {migration_result.error}",
|
|
2209
|
+
fg="red",
|
|
2210
|
+
err=True,
|
|
2211
|
+
)
|
|
2212
|
+
|
|
2213
|
+
# Exit code based on unresolved conflicts
|
|
2214
|
+
if result.unresolved_conflicts:
|
|
2215
|
+
click.echo()
|
|
2216
|
+
click.secho(
|
|
2217
|
+
f"⚠ {len(result.unresolved_conflicts)} unresolved conflict(s) "
|
|
2218
|
+
"marked in output",
|
|
2219
|
+
fg="yellow",
|
|
2220
|
+
)
|
|
2221
|
+
raise SystemExit(1)
|
|
2222
|
+
else:
|
|
2223
|
+
raise SystemExit(0)
|
|
2224
|
+
|
|
2225
|
+
|
|
2226
|
+
@cli.command()
|
|
2227
|
+
@click.argument("source", type=click.Path(exists=True, path_type=Path))
|
|
2228
|
+
@click.option(
|
|
2229
|
+
"--output",
|
|
2230
|
+
"-o",
|
|
2231
|
+
"output_dir",
|
|
2232
|
+
type=click.Path(path_type=Path),
|
|
2233
|
+
default=Path("modules"),
|
|
2234
|
+
help="Output directory for split modules (default: modules/)",
|
|
2235
|
+
)
|
|
2236
|
+
@click.option(
|
|
2237
|
+
"--config",
|
|
2238
|
+
"-c",
|
|
2239
|
+
"config_file",
|
|
2240
|
+
type=click.Path(exists=True, path_type=Path),
|
|
2241
|
+
help="YAML configuration file for split",
|
|
2242
|
+
)
|
|
2243
|
+
@click.option(
|
|
2244
|
+
"--by-namespace",
|
|
2245
|
+
is_flag=True,
|
|
2246
|
+
help="Automatically split by namespace (auto-detect modules)",
|
|
2247
|
+
)
|
|
2248
|
+
@click.option(
|
|
2249
|
+
"--migrate-data",
|
|
2250
|
+
multiple=True,
|
|
2251
|
+
type=click.Path(exists=True, path_type=Path),
|
|
2252
|
+
help="Data file(s) to split by instance type",
|
|
2253
|
+
)
|
|
2254
|
+
@click.option(
|
|
2255
|
+
"--data-output",
|
|
2256
|
+
type=click.Path(path_type=Path),
|
|
2257
|
+
help="Output directory for split data files",
|
|
2258
|
+
)
|
|
2259
|
+
@click.option(
|
|
2260
|
+
"--unmatched",
|
|
2261
|
+
type=click.Choice(["common", "error"], case_sensitive=False),
|
|
2262
|
+
default="common",
|
|
2263
|
+
help="Strategy for unmatched entities (default: common)",
|
|
2264
|
+
)
|
|
2265
|
+
@click.option(
|
|
2266
|
+
"--common-name",
|
|
2267
|
+
default="common",
|
|
2268
|
+
help="Name for common module (default: common)",
|
|
2269
|
+
)
|
|
2270
|
+
@click.option(
|
|
2271
|
+
"--no-manifest",
|
|
2272
|
+
is_flag=True,
|
|
2273
|
+
help="Don't generate manifest.yml",
|
|
2274
|
+
)
|
|
2275
|
+
@click.option(
|
|
2276
|
+
"--dry-run",
|
|
2277
|
+
is_flag=True,
|
|
2278
|
+
help="Show what would happen without writing files",
|
|
2279
|
+
)
|
|
2280
|
+
@click.option(
|
|
2281
|
+
"--no-colour",
|
|
2282
|
+
is_flag=True,
|
|
2283
|
+
help="Disable coloured output",
|
|
2284
|
+
)
|
|
2285
|
+
@click.option(
|
|
2286
|
+
"--init",
|
|
2287
|
+
"init_config",
|
|
2288
|
+
is_flag=True,
|
|
2289
|
+
help="Generate a default split configuration file",
|
|
2290
|
+
)
|
|
2291
|
+
def split(
|
|
2292
|
+
source: Path,
|
|
2293
|
+
output_dir: Path,
|
|
2294
|
+
config_file: Path | None,
|
|
2295
|
+
by_namespace: bool,
|
|
2296
|
+
migrate_data: tuple[Path, ...],
|
|
2297
|
+
data_output: Path | None,
|
|
2298
|
+
unmatched: str,
|
|
2299
|
+
common_name: str,
|
|
2300
|
+
no_manifest: bool,
|
|
2301
|
+
dry_run: bool,
|
|
2302
|
+
no_colour: bool,
|
|
2303
|
+
init_config: bool,
|
|
2304
|
+
):
|
|
2305
|
+
"""Split a monolithic ontology into multiple modules.
|
|
2306
|
+
|
|
2307
|
+
SOURCE: RDF ontology file to split (.ttl, .rdf, .owl)
|
|
2308
|
+
|
|
2309
|
+
\b
|
|
2310
|
+
Exit codes:
|
|
2311
|
+
0 - Split successful
|
|
2312
|
+
1 - Split successful with unmatched entities in common module
|
|
2313
|
+
2 - Error (file not found, config invalid, etc.)
|
|
2314
|
+
|
|
2315
|
+
\b
|
|
2316
|
+
Examples:
|
|
2317
|
+
# Split by namespace (auto-detect modules)
|
|
2318
|
+
rdf-construct split large.ttl -o modules/ --by-namespace
|
|
2319
|
+
|
|
2320
|
+
# Split using configuration file
|
|
2321
|
+
rdf-construct split large.ttl -o modules/ -c split.yml
|
|
2322
|
+
|
|
2323
|
+
# With data migration
|
|
2324
|
+
rdf-construct split large.ttl -o modules/ -c split.yml \\
|
|
2325
|
+
--migrate-data split_instances.ttl --data-output data/
|
|
2326
|
+
|
|
2327
|
+
# Dry run - show what would be created
|
|
2328
|
+
rdf-construct split large.ttl -o modules/ --by-namespace --dry-run
|
|
2329
|
+
|
|
2330
|
+
# Generate default config file
|
|
2331
|
+
rdf-construct split --init
|
|
2332
|
+
"""
|
|
2333
|
+
# Handle --init flag
|
|
2334
|
+
if init_config:
|
|
2335
|
+
config_path = Path("split.yml")
|
|
2336
|
+
if config_path.exists():
|
|
2337
|
+
click.secho(f"Config file already exists: {config_path}", fg="red", err=True)
|
|
2338
|
+
raise click.Abort()
|
|
2339
|
+
|
|
2340
|
+
config_content = create_default_split_config()
|
|
2341
|
+
config_path.write_text(config_content)
|
|
2342
|
+
click.secho(f"Created {config_path}", fg="green")
|
|
2343
|
+
click.echo("Edit this file to configure your split, then run:")
|
|
2344
|
+
click.echo(f" rdf-construct split your-ontology.ttl -c {config_path}")
|
|
2345
|
+
return
|
|
2346
|
+
|
|
2347
|
+
# Validate we have a source
|
|
2348
|
+
if not source:
|
|
2349
|
+
click.secho("Error: SOURCE is required.", fg="red", err=True)
|
|
2350
|
+
raise click.Abort()
|
|
2351
|
+
|
|
2352
|
+
# Handle --by-namespace mode
|
|
2353
|
+
if by_namespace:
|
|
2354
|
+
click.echo(f"Splitting {source.name} by namespace...")
|
|
2355
|
+
|
|
2356
|
+
result = split_by_namespace(source, output_dir, dry_run=dry_run)
|
|
2357
|
+
|
|
2358
|
+
if not result.success:
|
|
2359
|
+
click.secho(f"✗ Split failed: {result.error}", fg="red", err=True)
|
|
2360
|
+
raise SystemExit(2)
|
|
2361
|
+
|
|
2362
|
+
_display_split_result(result, output_dir, dry_run, not no_colour)
|
|
2363
|
+
raise SystemExit(0 if not result.unmatched_entities else 1)
|
|
2364
|
+
|
|
2365
|
+
# Build configuration from file or CLI
|
|
2366
|
+
if config_file:
|
|
2367
|
+
try:
|
|
2368
|
+
config = SplitConfig.from_yaml(config_file)
|
|
2369
|
+
# Override source and output_dir if provided
|
|
2370
|
+
config.source = source
|
|
2371
|
+
config.output_dir = output_dir
|
|
2372
|
+
config.dry_run = dry_run
|
|
2373
|
+
config.generate_manifest = not no_manifest
|
|
2374
|
+
click.echo(f"Using config: {config_file}")
|
|
2375
|
+
except (FileNotFoundError, ValueError) as e:
|
|
2376
|
+
click.secho(f"Error loading config: {e}", fg="red", err=True)
|
|
2377
|
+
raise click.Abort()
|
|
2378
|
+
else:
|
|
2379
|
+
# Need either --by-namespace or --config
|
|
2380
|
+
if not by_namespace:
|
|
2381
|
+
click.secho(
|
|
2382
|
+
"Error: Specify either --by-namespace or --config.",
|
|
2383
|
+
fg="red",
|
|
2384
|
+
err=True,
|
|
2385
|
+
)
|
|
2386
|
+
click.echo("Use --by-namespace for auto-detection or -c for a config file.")
|
|
2387
|
+
click.echo("Run 'rdf-construct split --init' to generate a config template.")
|
|
2388
|
+
raise click.Abort()
|
|
2389
|
+
|
|
2390
|
+
# Build minimal config
|
|
2391
|
+
config = SplitConfig(
|
|
2392
|
+
source=source,
|
|
2393
|
+
output_dir=output_dir,
|
|
2394
|
+
modules=[],
|
|
2395
|
+
unmatched=UnmatchedStrategy(
|
|
2396
|
+
strategy=unmatched,
|
|
2397
|
+
common_module=common_name,
|
|
2398
|
+
common_output=f"{common_name}.ttl",
|
|
2399
|
+
),
|
|
2400
|
+
generate_manifest=not no_manifest,
|
|
2401
|
+
dry_run=dry_run,
|
|
2402
|
+
)
|
|
2403
|
+
|
|
2404
|
+
# Add data migration config if specified
|
|
2405
|
+
if migrate_data:
|
|
2406
|
+
config.split_data = SplitDataConfig(
|
|
2407
|
+
sources=list(migrate_data),
|
|
2408
|
+
output_dir=data_output if data_output else output_dir,
|
|
2409
|
+
prefix="data_",
|
|
2410
|
+
)
|
|
2411
|
+
|
|
2412
|
+
# Override unmatched strategy if specified on CLI
|
|
2413
|
+
if unmatched:
|
|
2414
|
+
config.unmatched = UnmatchedStrategy(
|
|
2415
|
+
strategy=unmatched,
|
|
2416
|
+
common_module=common_name,
|
|
2417
|
+
common_output=f"{common_name}.ttl",
|
|
2418
|
+
)
|
|
2419
|
+
|
|
2420
|
+
# Execute split
|
|
2421
|
+
click.echo(f"Splitting {source.name}...")
|
|
2422
|
+
|
|
2423
|
+
splitter = OntologySplitter(config)
|
|
2424
|
+
result = splitter.split()
|
|
2425
|
+
|
|
2426
|
+
if not result.success:
|
|
2427
|
+
click.secho(f"✗ Split failed: {result.error}", fg="red", err=True)
|
|
2428
|
+
raise SystemExit(2)
|
|
2429
|
+
|
|
2430
|
+
# Write output (unless dry run)
|
|
2431
|
+
if not dry_run:
|
|
2432
|
+
splitter.write_modules(result)
|
|
2433
|
+
if config.generate_manifest:
|
|
2434
|
+
splitter.write_manifest(result)
|
|
2435
|
+
|
|
2436
|
+
_display_split_result(result, output_dir, dry_run, not no_colour)
|
|
2437
|
+
|
|
2438
|
+
# Exit code based on unmatched entities
|
|
2439
|
+
if result.unmatched_entities and config.unmatched.strategy == "common":
|
|
2440
|
+
click.echo()
|
|
2441
|
+
click.secho(
|
|
2442
|
+
f"⚠ {len(result.unmatched_entities)} unmatched entities placed in "
|
|
2443
|
+
f"{config.unmatched.common_module} module",
|
|
2444
|
+
fg="yellow",
|
|
2445
|
+
)
|
|
2446
|
+
raise SystemExit(1)
|
|
2447
|
+
else:
|
|
2448
|
+
raise SystemExit(0)
|
|
2449
|
+
|
|
2450
|
+
|
|
2451
|
+
def _display_split_result(
|
|
2452
|
+
result: "SplitResult",
|
|
2453
|
+
output_dir: Path,
|
|
2454
|
+
dry_run: bool,
|
|
2455
|
+
use_colour: bool,
|
|
2456
|
+
) -> None:
|
|
2457
|
+
"""Display split results to console.
|
|
2458
|
+
|
|
2459
|
+
Args:
|
|
2460
|
+
result: SplitResult from split operation.
|
|
2461
|
+
output_dir: Output directory.
|
|
2462
|
+
dry_run: Whether this was a dry run.
|
|
2463
|
+
use_colour: Whether to use coloured output.
|
|
2464
|
+
"""
|
|
2465
|
+
# Header
|
|
2466
|
+
if dry_run:
|
|
2467
|
+
click.echo("\n[DRY RUN] Would create:")
|
|
2468
|
+
else:
|
|
2469
|
+
click.echo("\nSplit complete:")
|
|
2470
|
+
|
|
2471
|
+
# Module summary
|
|
2472
|
+
click.echo(f"\n Modules: {result.total_modules}")
|
|
2473
|
+
click.echo(f" Total triples: {result.total_triples}")
|
|
2474
|
+
|
|
2475
|
+
# Module details
|
|
2476
|
+
if result.module_stats:
|
|
2477
|
+
click.echo("\n Module breakdown:")
|
|
2478
|
+
for stats in result.module_stats:
|
|
2479
|
+
deps_str = ""
|
|
2480
|
+
if stats.dependencies:
|
|
2481
|
+
deps_str = f" (deps: {', '.join(stats.dependencies)})"
|
|
2482
|
+
click.echo(
|
|
2483
|
+
f" {stats.file}: {stats.classes} classes, "
|
|
2484
|
+
f"{stats.properties} properties, {stats.triples} triples{deps_str}"
|
|
2485
|
+
)
|
|
2486
|
+
|
|
2487
|
+
# Unmatched entities
|
|
2488
|
+
if result.unmatched_entities:
|
|
2489
|
+
click.echo(f"\n Unmatched entities: {len(result.unmatched_entities)}")
|
|
2490
|
+
# Show first few
|
|
2491
|
+
sample = list(result.unmatched_entities)[:5]
|
|
2492
|
+
for uri in sample:
|
|
2493
|
+
click.echo(f" - {uri}")
|
|
2494
|
+
if len(result.unmatched_entities) > 5:
|
|
2495
|
+
click.echo(f" ... and {len(result.unmatched_entities) - 5} more")
|
|
2496
|
+
|
|
2497
|
+
# Output location
|
|
2498
|
+
if not dry_run:
|
|
2499
|
+
click.echo()
|
|
2500
|
+
if use_colour:
|
|
2501
|
+
click.secho(f"✓ Wrote modules to {output_dir}/", fg="green")
|
|
2502
|
+
else:
|
|
2503
|
+
click.echo(f"✓ Wrote modules to {output_dir}/")
|
|
2504
|
+
|
|
2505
|
+
|
|
2506
|
+
# Refactor command group
|
|
2507
|
+
@cli.group()
|
|
2508
|
+
def refactor():
|
|
2509
|
+
"""Refactor ontologies: rename URIs and deprecate entities.
|
|
2510
|
+
|
|
2511
|
+
\b
|
|
2512
|
+
Subcommands:
|
|
2513
|
+
rename Rename URIs (single entity or bulk namespace)
|
|
2514
|
+
deprecate Mark entities as deprecated
|
|
2515
|
+
|
|
2516
|
+
\b
|
|
2517
|
+
Examples:
|
|
2518
|
+
# Fix a typo
|
|
2519
|
+
rdf-construct refactor rename ont.ttl --from ex:Buiding --to ex:Building -o fixed.ttl
|
|
2520
|
+
|
|
2521
|
+
# Bulk namespace change
|
|
2522
|
+
rdf-construct refactor rename ont.ttl \\
|
|
2523
|
+
--from-namespace http://old/ --to-namespace http://new/ -o migrated.ttl
|
|
2524
|
+
|
|
2525
|
+
# Deprecate entity with replacement
|
|
2526
|
+
rdf-construct refactor deprecate ont.ttl \\
|
|
2527
|
+
--entity ex:OldClass --replaced-by ex:NewClass \\
|
|
2528
|
+
--message "Use NewClass instead." -o updated.ttl
|
|
2529
|
+
"""
|
|
2530
|
+
pass
|
|
2531
|
+
|
|
2532
|
+
|
|
2533
|
+
@refactor.command("rename")
|
|
2534
|
+
@click.argument("sources", nargs=-1, type=click.Path(exists=True, path_type=Path))
|
|
2535
|
+
@click.option(
|
|
2536
|
+
"-o", "--output",
|
|
2537
|
+
type=click.Path(path_type=Path),
|
|
2538
|
+
help="Output file (for single source) or directory (for multiple sources).",
|
|
2539
|
+
)
|
|
2540
|
+
@click.option(
|
|
2541
|
+
"--from", "from_uri",
|
|
2542
|
+
help="Single URI to rename (use with --to).",
|
|
2543
|
+
)
|
|
2544
|
+
@click.option(
|
|
2545
|
+
"--to", "to_uri",
|
|
2546
|
+
help="New URI for single rename (use with --from).",
|
|
2547
|
+
)
|
|
2548
|
+
@click.option(
|
|
2549
|
+
"--from-namespace",
|
|
2550
|
+
help="Old namespace prefix for bulk rename.",
|
|
2551
|
+
)
|
|
2552
|
+
@click.option(
|
|
2553
|
+
"--to-namespace",
|
|
2554
|
+
help="New namespace prefix for bulk rename.",
|
|
2555
|
+
)
|
|
2556
|
+
@click.option(
|
|
2557
|
+
"-c", "--config",
|
|
2558
|
+
"config_file",
|
|
2559
|
+
type=click.Path(exists=True, path_type=Path),
|
|
2560
|
+
help="YAML configuration file with rename mappings.",
|
|
2561
|
+
)
|
|
2562
|
+
@click.option(
|
|
2563
|
+
"--migrate-data",
|
|
2564
|
+
multiple=True,
|
|
2565
|
+
type=click.Path(exists=True, path_type=Path),
|
|
2566
|
+
help="Data files to migrate (can be repeated).",
|
|
2567
|
+
)
|
|
2568
|
+
@click.option(
|
|
2569
|
+
"--data-output",
|
|
2570
|
+
type=click.Path(path_type=Path),
|
|
2571
|
+
help="Output path for migrated data.",
|
|
2572
|
+
)
|
|
2573
|
+
@click.option(
|
|
2574
|
+
"--dry-run",
|
|
2575
|
+
is_flag=True,
|
|
2576
|
+
help="Preview changes without writing files.",
|
|
2577
|
+
)
|
|
2578
|
+
@click.option(
|
|
2579
|
+
"--no-colour", "--no-color",
|
|
2580
|
+
is_flag=True,
|
|
2581
|
+
help="Disable coloured output.",
|
|
2582
|
+
)
|
|
2583
|
+
@click.option(
|
|
2584
|
+
"--init",
|
|
2585
|
+
"init_config",
|
|
2586
|
+
is_flag=True,
|
|
2587
|
+
help="Generate a template rename configuration file.",
|
|
2588
|
+
)
|
|
2589
|
+
def refactor_rename(
|
|
2590
|
+
sources: tuple[Path, ...],
|
|
2591
|
+
output: Path | None,
|
|
2592
|
+
from_uri: str | None,
|
|
2593
|
+
to_uri: str | None,
|
|
2594
|
+
from_namespace: str | None,
|
|
2595
|
+
to_namespace: str | None,
|
|
2596
|
+
config_file: Path | None,
|
|
2597
|
+
migrate_data: tuple[Path, ...],
|
|
2598
|
+
data_output: Path | None,
|
|
2599
|
+
dry_run: bool,
|
|
2600
|
+
no_colour: bool,
|
|
2601
|
+
init_config: bool,
|
|
2602
|
+
):
|
|
2603
|
+
"""Rename URIs in ontology files.
|
|
2604
|
+
|
|
2605
|
+
Supports single entity renames (fixing typos) and bulk namespace changes
|
|
2606
|
+
(project migrations). The renamer updates subject, predicate, and object
|
|
2607
|
+
positions but intentionally leaves literal values unchanged.
|
|
2608
|
+
|
|
2609
|
+
\b
|
|
2610
|
+
SOURCES: One or more RDF files to process (.ttl, .rdf, .owl)
|
|
2611
|
+
|
|
2612
|
+
\b
|
|
2613
|
+
Exit codes:
|
|
2614
|
+
0 - Success
|
|
2615
|
+
1 - Success with warnings (some URIs not found)
|
|
2616
|
+
2 - Error (file not found, parse error, etc.)
|
|
2617
|
+
|
|
2618
|
+
\b
|
|
2619
|
+
Examples:
|
|
2620
|
+
# Fix a single typo
|
|
2621
|
+
rdf-construct refactor rename ontology.ttl \\
|
|
2622
|
+
--from "http://example.org/ont#Buiding" \\
|
|
2623
|
+
--to "http://example.org/ont#Building" \\
|
|
2624
|
+
-o fixed.ttl
|
|
2625
|
+
|
|
2626
|
+
# Bulk namespace change
|
|
2627
|
+
rdf-construct refactor rename ontology.ttl \\
|
|
2628
|
+
--from-namespace "http://old.example.org/" \\
|
|
2629
|
+
--to-namespace "http://new.example.org/" \\
|
|
2630
|
+
-o migrated.ttl
|
|
2631
|
+
|
|
2632
|
+
# With data migration
|
|
2633
|
+
rdf-construct refactor rename ontology.ttl \\
|
|
2634
|
+
--from "ex:OldClass" --to "ex:NewClass" \\
|
|
2635
|
+
--migrate-data instances.ttl \\
|
|
2636
|
+
--data-output updated-instances.ttl
|
|
2637
|
+
|
|
2638
|
+
# From configuration file
|
|
2639
|
+
rdf-construct refactor rename --config renames.yml
|
|
2640
|
+
|
|
2641
|
+
# Preview changes (dry run)
|
|
2642
|
+
rdf-construct refactor rename ontology.ttl \\
|
|
2643
|
+
--from "ex:Old" --to "ex:New" --dry-run
|
|
2644
|
+
|
|
2645
|
+
# Process multiple files
|
|
2646
|
+
rdf-construct refactor rename modules/*.ttl \\
|
|
2647
|
+
--from-namespace "http://old/" --to-namespace "http://new/" \\
|
|
2648
|
+
-o migrated/
|
|
2649
|
+
|
|
2650
|
+
# Generate template config
|
|
2651
|
+
rdf-construct refactor rename --init
|
|
2652
|
+
"""
|
|
2653
|
+
# Handle --init flag
|
|
2654
|
+
if init_config:
|
|
2655
|
+
config_path = Path("refactor_rename.yml")
|
|
2656
|
+
if config_path.exists():
|
|
2657
|
+
click.secho(f"Config file already exists: {config_path}", fg="red", err=True)
|
|
2658
|
+
raise click.Abort()
|
|
2659
|
+
|
|
2660
|
+
config_content = create_default_rename_config()
|
|
2661
|
+
config_path.write_text(config_content)
|
|
2662
|
+
click.secho(f"Created {config_path}", fg="green")
|
|
2663
|
+
click.echo("Edit this file to configure your renames, then run:")
|
|
2664
|
+
click.echo(f" rdf-construct refactor rename --config {config_path}")
|
|
2665
|
+
return
|
|
2666
|
+
|
|
2667
|
+
# Validate input options
|
|
2668
|
+
if not sources and not config_file:
|
|
2669
|
+
click.secho("Error: No source files specified.", fg="red", err=True)
|
|
2670
|
+
click.echo("Provide source files or use --config with a configuration file.", err=True)
|
|
2671
|
+
raise click.Abort()
|
|
2672
|
+
|
|
2673
|
+
# Validate rename options
|
|
2674
|
+
if from_uri and not to_uri:
|
|
2675
|
+
click.secho("Error: --from requires --to", fg="red", err=True)
|
|
2676
|
+
raise click.Abort()
|
|
2677
|
+
if to_uri and not from_uri:
|
|
2678
|
+
click.secho("Error: --to requires --from", fg="red", err=True)
|
|
2679
|
+
raise click.Abort()
|
|
2680
|
+
if from_namespace and not to_namespace:
|
|
2681
|
+
click.secho("Error: --from-namespace requires --to-namespace", fg="red", err=True)
|
|
2682
|
+
raise click.Abort()
|
|
2683
|
+
if to_namespace and not from_namespace:
|
|
2684
|
+
click.secho("Error: --to-namespace requires --from-namespace", fg="red", err=True)
|
|
2685
|
+
raise click.Abort()
|
|
2686
|
+
|
|
2687
|
+
# Build configuration
|
|
2688
|
+
if config_file:
|
|
2689
|
+
try:
|
|
2690
|
+
config = load_refactor_config(config_file)
|
|
2691
|
+
click.echo(f"Using config: {config_file}")
|
|
2692
|
+
|
|
2693
|
+
# Override output if provided on CLI
|
|
2694
|
+
if output:
|
|
2695
|
+
if len(sources) > 1 or (config.source_files and len(config.source_files) > 1):
|
|
2696
|
+
config.output_dir = output
|
|
2697
|
+
else:
|
|
2698
|
+
config.output = output
|
|
2699
|
+
|
|
2700
|
+
# Override sources if provided on CLI
|
|
2701
|
+
if sources:
|
|
2702
|
+
config.source_files = list(sources)
|
|
2703
|
+
except (FileNotFoundError, ValueError) as e:
|
|
2704
|
+
click.secho(f"Error loading config: {e}", fg="red", err=True)
|
|
2705
|
+
raise click.Abort()
|
|
2706
|
+
else:
|
|
2707
|
+
# Build config from CLI arguments
|
|
2708
|
+
rename_config = RenameConfig()
|
|
2709
|
+
|
|
2710
|
+
if from_namespace and to_namespace:
|
|
2711
|
+
rename_config.namespaces[from_namespace] = to_namespace
|
|
2712
|
+
|
|
2713
|
+
if from_uri and to_uri:
|
|
2714
|
+
# Expand CURIEs if needed
|
|
2715
|
+
rename_config.entities[from_uri] = to_uri
|
|
2716
|
+
|
|
2717
|
+
config = RefactorConfig(
|
|
2718
|
+
rename=rename_config,
|
|
2719
|
+
source_files=list(sources),
|
|
2720
|
+
output=output if len(sources) == 1 else None,
|
|
2721
|
+
output_dir=output if len(sources) > 1 else None,
|
|
2722
|
+
dry_run=dry_run,
|
|
2723
|
+
)
|
|
2724
|
+
|
|
2725
|
+
# Validate we have something to rename
|
|
2726
|
+
if config.rename is None or (not config.rename.namespaces and not config.rename.entities):
|
|
2727
|
+
click.secho(
|
|
2728
|
+
"Error: No renames specified. Use --from/--to, --from-namespace/--to-namespace, "
|
|
2729
|
+
"or provide a config file.",
|
|
2730
|
+
fg="red",
|
|
2731
|
+
err=True,
|
|
2732
|
+
)
|
|
2733
|
+
raise click.Abort()
|
|
2734
|
+
|
|
2735
|
+
# Execute rename
|
|
2736
|
+
formatter = RefactorTextFormatter(use_colour=not no_colour)
|
|
2737
|
+
renamer = OntologyRenamer()
|
|
2738
|
+
|
|
2739
|
+
for source_path in config.source_files:
|
|
2740
|
+
click.echo(f"\nProcessing: {source_path}")
|
|
2741
|
+
|
|
2742
|
+
# Load source graph
|
|
2743
|
+
graph = Graph()
|
|
2744
|
+
try:
|
|
2745
|
+
graph.parse(source_path.as_posix())
|
|
2746
|
+
except Exception as e:
|
|
2747
|
+
click.secho(f"✗ Failed to parse: {e}", fg="red", err=True)
|
|
2748
|
+
raise SystemExit(2)
|
|
2749
|
+
|
|
2750
|
+
# Build mappings for preview
|
|
2751
|
+
mappings = config.rename.build_mappings(graph)
|
|
2752
|
+
|
|
2753
|
+
if dry_run:
|
|
2754
|
+
# Show preview
|
|
2755
|
+
click.echo()
|
|
2756
|
+
click.echo(
|
|
2757
|
+
formatter.format_rename_preview(
|
|
2758
|
+
mappings=mappings,
|
|
2759
|
+
source_file=source_path.name,
|
|
2760
|
+
source_triples=len(graph),
|
|
2761
|
+
)
|
|
2762
|
+
)
|
|
2763
|
+
else:
|
|
2764
|
+
# Perform rename
|
|
2765
|
+
result = renamer.rename(graph, config.rename)
|
|
2766
|
+
|
|
2767
|
+
if not result.success:
|
|
2768
|
+
click.secho(f"✗ Rename failed: {result.error}", fg="red", err=True)
|
|
2769
|
+
raise SystemExit(2)
|
|
2770
|
+
|
|
2771
|
+
# Show result
|
|
2772
|
+
click.echo(formatter.format_rename_result(result))
|
|
2773
|
+
|
|
2774
|
+
# Write output
|
|
2775
|
+
if result.renamed_graph:
|
|
2776
|
+
out_path = config.output or (config.output_dir / source_path.name if config.output_dir else None)
|
|
2777
|
+
if out_path:
|
|
2778
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2779
|
+
result.renamed_graph.serialize(destination=out_path.as_posix(), format="turtle")
|
|
2780
|
+
click.secho(f"✓ Wrote {out_path}", fg="green")
|
|
2781
|
+
|
|
2782
|
+
# Handle data migration
|
|
2783
|
+
if migrate_data and not dry_run:
|
|
2784
|
+
click.echo("\nMigrating data...")
|
|
2785
|
+
|
|
2786
|
+
# Build URI map from rename config
|
|
2787
|
+
combined_graph = Graph()
|
|
2788
|
+
for source_path in config.source_files:
|
|
2789
|
+
combined_graph.parse(source_path.as_posix())
|
|
2790
|
+
|
|
2791
|
+
uri_map = {}
|
|
2792
|
+
for mapping in config.rename.build_mappings(combined_graph):
|
|
2793
|
+
uri_map[mapping.from_uri] = mapping.to_uri
|
|
2794
|
+
|
|
2795
|
+
if uri_map:
|
|
2796
|
+
migrator = DataMigrator()
|
|
2797
|
+
for data_path in migrate_data:
|
|
2798
|
+
data_graph = Graph()
|
|
2799
|
+
try:
|
|
2800
|
+
data_graph.parse(data_path.as_posix())
|
|
2801
|
+
except Exception as e:
|
|
2802
|
+
click.secho(f"✗ Failed to parse data file {data_path}: {e}", fg="red", err=True)
|
|
2803
|
+
continue
|
|
2804
|
+
|
|
2805
|
+
migration_result = migrator.migrate(data_graph, uri_map=uri_map)
|
|
2806
|
+
|
|
2807
|
+
if migration_result.success and migration_result.migrated_graph:
|
|
2808
|
+
# Determine output path
|
|
2809
|
+
if data_output and len(migrate_data) == 1:
|
|
2810
|
+
out_path = data_output
|
|
2811
|
+
elif data_output:
|
|
2812
|
+
out_path = data_output / data_path.name
|
|
2813
|
+
else:
|
|
2814
|
+
out_path = data_path.parent / f"migrated_{data_path.name}"
|
|
2815
|
+
|
|
2816
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2817
|
+
migration_result.migrated_graph.serialize(
|
|
2818
|
+
destination=out_path.as_posix(), format="turtle"
|
|
2819
|
+
)
|
|
2820
|
+
click.echo(f" Migrated {data_path.name}: {migration_result.stats.total_changes} changes")
|
|
2821
|
+
click.secho(f" ✓ Wrote {out_path}", fg="green")
|
|
2822
|
+
|
|
2823
|
+
raise SystemExit(0)
|
|
2824
|
+
|
|
2825
|
+
|
|
2826
|
+
@refactor.command("deprecate")
|
|
2827
|
+
@click.argument("sources", nargs=-1, type=click.Path(exists=True, path_type=Path))
|
|
2828
|
+
@click.option(
|
|
2829
|
+
"-o", "--output",
|
|
2830
|
+
type=click.Path(path_type=Path),
|
|
2831
|
+
help="Output file.",
|
|
2832
|
+
)
|
|
2833
|
+
@click.option(
|
|
2834
|
+
"--entity",
|
|
2835
|
+
help="URI of entity to deprecate.",
|
|
2836
|
+
)
|
|
2837
|
+
@click.option(
|
|
2838
|
+
"--replaced-by",
|
|
2839
|
+
help="URI of replacement entity (adds dcterms:isReplacedBy).",
|
|
2840
|
+
)
|
|
2841
|
+
@click.option(
|
|
2842
|
+
"--message", "-m",
|
|
2843
|
+
help="Deprecation message (added to rdfs:comment).",
|
|
2844
|
+
)
|
|
2845
|
+
@click.option(
|
|
2846
|
+
"--version",
|
|
2847
|
+
help="Version when deprecated (included in message).",
|
|
2848
|
+
)
|
|
2849
|
+
@click.option(
|
|
2850
|
+
"-c", "--config",
|
|
2851
|
+
"config_file",
|
|
2852
|
+
type=click.Path(exists=True, path_type=Path),
|
|
2853
|
+
help="YAML configuration file with deprecation specs.",
|
|
2854
|
+
)
|
|
2855
|
+
@click.option(
|
|
2856
|
+
"--dry-run",
|
|
2857
|
+
is_flag=True,
|
|
2858
|
+
help="Preview changes without writing files.",
|
|
2859
|
+
)
|
|
2860
|
+
@click.option(
|
|
2861
|
+
"--no-colour", "--no-color",
|
|
2862
|
+
is_flag=True,
|
|
2863
|
+
help="Disable coloured output.",
|
|
2864
|
+
)
|
|
2865
|
+
@click.option(
|
|
2866
|
+
"--init",
|
|
2867
|
+
"init_config",
|
|
2868
|
+
is_flag=True,
|
|
2869
|
+
help="Generate a template deprecation configuration file.",
|
|
2870
|
+
)
|
|
2871
|
+
def refactor_deprecate(
|
|
2872
|
+
sources: tuple[Path, ...],
|
|
2873
|
+
output: Path | None,
|
|
2874
|
+
entity: str | None,
|
|
2875
|
+
replaced_by: str | None,
|
|
2876
|
+
message: str | None,
|
|
2877
|
+
version: str | None,
|
|
2878
|
+
config_file: Path | None,
|
|
2879
|
+
dry_run: bool,
|
|
2880
|
+
no_colour: bool,
|
|
2881
|
+
init_config: bool,
|
|
2882
|
+
):
|
|
2883
|
+
"""Mark ontology entities as deprecated.
|
|
2884
|
+
|
|
2885
|
+
Adds standard deprecation annotations:
|
|
2886
|
+
- owl:deprecated true
|
|
2887
|
+
- dcterms:isReplacedBy (if replacement specified)
|
|
2888
|
+
- rdfs:comment with "DEPRECATED: ..." message
|
|
2889
|
+
|
|
2890
|
+
Deprecation marks entities but does NOT rename or migrate references.
|
|
2891
|
+
Use 'refactor rename' to actually migrate references after deprecation.
|
|
2892
|
+
|
|
2893
|
+
\b
|
|
2894
|
+
SOURCES: One or more RDF files to process (.ttl, .rdf, .owl)
|
|
2895
|
+
|
|
2896
|
+
\b
|
|
2897
|
+
Exit codes:
|
|
2898
|
+
0 - Success
|
|
2899
|
+
1 - Success with warnings (some entities not found)
|
|
2900
|
+
2 - Error (file not found, parse error, etc.)
|
|
2901
|
+
|
|
2902
|
+
\b
|
|
2903
|
+
Examples:
|
|
2904
|
+
# Deprecate with replacement
|
|
2905
|
+
rdf-construct refactor deprecate ontology.ttl \\
|
|
2906
|
+
--entity "http://example.org/ont#LegacyTerm" \\
|
|
2907
|
+
--replaced-by "http://example.org/ont#NewTerm" \\
|
|
2908
|
+
--message "Use NewTerm instead. Will be removed in v3.0." \\
|
|
2909
|
+
-o updated.ttl
|
|
2910
|
+
|
|
2911
|
+
# Deprecate without replacement
|
|
2912
|
+
rdf-construct refactor deprecate ontology.ttl \\
|
|
2913
|
+
--entity "ex:ObsoleteThing" \\
|
|
2914
|
+
--message "No longer needed. Will be removed in v3.0." \\
|
|
2915
|
+
-o updated.ttl
|
|
2916
|
+
|
|
2917
|
+
# Bulk deprecation from config
|
|
2918
|
+
rdf-construct refactor deprecate ontology.ttl \\
|
|
2919
|
+
-c deprecations.yml \\
|
|
2920
|
+
-o updated.ttl
|
|
2921
|
+
|
|
2922
|
+
# Preview changes (dry run)
|
|
2923
|
+
rdf-construct refactor deprecate ontology.ttl \\
|
|
2924
|
+
--entity "ex:Legacy" --replaced-by "ex:Modern" --dry-run
|
|
2925
|
+
|
|
2926
|
+
# Generate template config
|
|
2927
|
+
rdf-construct refactor deprecate --init
|
|
2928
|
+
"""
|
|
2929
|
+
# Handle --init flag
|
|
2930
|
+
if init_config:
|
|
2931
|
+
config_path = Path("refactor_deprecate.yml")
|
|
2932
|
+
if config_path.exists():
|
|
2933
|
+
click.secho(f"Config file already exists: {config_path}", fg="red", err=True)
|
|
2934
|
+
raise click.Abort()
|
|
2935
|
+
|
|
2936
|
+
config_content = create_default_deprecation_config()
|
|
2937
|
+
config_path.write_text(config_content)
|
|
2938
|
+
click.secho(f"Created {config_path}", fg="green")
|
|
2939
|
+
click.echo("Edit this file to configure your deprecations, then run:")
|
|
2940
|
+
click.echo(f" rdf-construct refactor deprecate --config {config_path}")
|
|
2941
|
+
return
|
|
2942
|
+
|
|
2943
|
+
# Validate input options
|
|
2944
|
+
if not sources and not config_file:
|
|
2945
|
+
click.secho("Error: No source files specified.", fg="red", err=True)
|
|
2946
|
+
click.echo("Provide source files or use --config with a configuration file.", err=True)
|
|
2947
|
+
raise click.Abort()
|
|
2948
|
+
|
|
2949
|
+
# Build configuration
|
|
2950
|
+
if config_file:
|
|
2951
|
+
try:
|
|
2952
|
+
config = load_refactor_config(config_file)
|
|
2953
|
+
click.echo(f"Using config: {config_file}")
|
|
2954
|
+
|
|
2955
|
+
# Override output if provided on CLI
|
|
2956
|
+
if output:
|
|
2957
|
+
config.output = output
|
|
2958
|
+
|
|
2959
|
+
# Override sources if provided on CLI
|
|
2960
|
+
if sources:
|
|
2961
|
+
config.source_files = list(sources)
|
|
2962
|
+
except (FileNotFoundError, ValueError) as e:
|
|
2963
|
+
click.secho(f"Error loading config: {e}", fg="red", err=True)
|
|
2964
|
+
raise click.Abort()
|
|
2965
|
+
else:
|
|
2966
|
+
# Build config from CLI arguments
|
|
2967
|
+
if not entity:
|
|
2968
|
+
click.secho(
|
|
2969
|
+
"Error: --entity is required when not using a config file.",
|
|
2970
|
+
fg="red",
|
|
2971
|
+
err=True,
|
|
2972
|
+
)
|
|
2973
|
+
raise click.Abort()
|
|
2974
|
+
|
|
2975
|
+
spec = DeprecationSpec(
|
|
2976
|
+
entity=entity,
|
|
2977
|
+
replaced_by=replaced_by,
|
|
2978
|
+
message=message,
|
|
2979
|
+
version=version,
|
|
2980
|
+
)
|
|
2981
|
+
|
|
2982
|
+
config = RefactorConfig(
|
|
2983
|
+
deprecations=[spec],
|
|
2984
|
+
source_files=list(sources),
|
|
2985
|
+
output=output,
|
|
2986
|
+
dry_run=dry_run,
|
|
2987
|
+
)
|
|
2988
|
+
|
|
2989
|
+
# Validate we have something to deprecate
|
|
2990
|
+
if not config.deprecations:
|
|
2991
|
+
click.secho(
|
|
2992
|
+
"Error: No deprecations specified. Use --entity or provide a config file.",
|
|
2993
|
+
fg="red",
|
|
2994
|
+
err=True,
|
|
2995
|
+
)
|
|
2996
|
+
raise click.Abort()
|
|
2997
|
+
|
|
2998
|
+
# Execute deprecation
|
|
2999
|
+
formatter = RefactorTextFormatter(use_colour=not no_colour)
|
|
3000
|
+
deprecator = OntologyDeprecator()
|
|
3001
|
+
|
|
3002
|
+
for source_path in config.source_files:
|
|
3003
|
+
click.echo(f"\nProcessing: {source_path}")
|
|
3004
|
+
|
|
3005
|
+
# Load source graph
|
|
3006
|
+
graph = Graph()
|
|
3007
|
+
try:
|
|
3008
|
+
graph.parse(source_path.as_posix())
|
|
3009
|
+
except Exception as e:
|
|
3010
|
+
click.secho(f"✗ Failed to parse: {e}", fg="red", err=True)
|
|
3011
|
+
raise SystemExit(2)
|
|
3012
|
+
|
|
3013
|
+
if dry_run:
|
|
3014
|
+
# Perform dry run to get entity info
|
|
3015
|
+
temp_graph = Graph()
|
|
3016
|
+
for triple in graph:
|
|
3017
|
+
temp_graph.add(triple)
|
|
3018
|
+
|
|
3019
|
+
result = deprecator.deprecate_bulk(temp_graph, config.deprecations)
|
|
3020
|
+
|
|
3021
|
+
# Show preview
|
|
3022
|
+
click.echo()
|
|
3023
|
+
click.echo(
|
|
3024
|
+
formatter.format_deprecation_preview(
|
|
3025
|
+
specs=config.deprecations,
|
|
3026
|
+
entity_info=result.entity_info,
|
|
3027
|
+
source_file=source_path.name,
|
|
3028
|
+
source_triples=len(graph),
|
|
3029
|
+
)
|
|
3030
|
+
)
|
|
3031
|
+
else:
|
|
3032
|
+
# Perform deprecation
|
|
3033
|
+
result = deprecator.deprecate_bulk(graph, config.deprecations)
|
|
3034
|
+
|
|
3035
|
+
if not result.success:
|
|
3036
|
+
click.secho(f"✗ Deprecation failed: {result.error}", fg="red", err=True)
|
|
3037
|
+
raise SystemExit(2)
|
|
3038
|
+
|
|
3039
|
+
# Show result
|
|
3040
|
+
click.echo(formatter.format_deprecation_result(result))
|
|
3041
|
+
|
|
3042
|
+
# Write output
|
|
3043
|
+
if result.deprecated_graph:
|
|
3044
|
+
out_path = config.output or source_path.with_stem(f"{source_path.stem}_deprecated")
|
|
3045
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
3046
|
+
result.deprecated_graph.serialize(destination=out_path.as_posix(), format="turtle")
|
|
3047
|
+
click.secho(f"✓ Wrote {out_path}", fg="green")
|
|
3048
|
+
|
|
3049
|
+
# Warn about entities not found
|
|
3050
|
+
if result.stats.entities_not_found > 0:
|
|
3051
|
+
click.secho(
|
|
3052
|
+
f"\n⚠ {result.stats.entities_not_found} entity/entities not found in graph",
|
|
3053
|
+
fg="yellow",
|
|
3054
|
+
)
|
|
3055
|
+
raise SystemExit(1)
|
|
3056
|
+
|
|
3057
|
+
raise SystemExit(0)
|
|
3058
|
+
|
|
3059
|
+
|
|
3060
|
+
@cli.group()
|
|
3061
|
+
def localise():
|
|
3062
|
+
"""Multi-language translation management.
|
|
3063
|
+
|
|
3064
|
+
Extract translatable strings, merge translations, and track coverage.
|
|
3065
|
+
|
|
3066
|
+
\b
|
|
3067
|
+
Commands:
|
|
3068
|
+
extract Extract strings for translation
|
|
3069
|
+
merge Merge translations back into ontology
|
|
3070
|
+
report Generate translation coverage report
|
|
3071
|
+
init Create empty translation file for new language
|
|
3072
|
+
|
|
3073
|
+
\b
|
|
3074
|
+
Examples:
|
|
3075
|
+
# Extract strings for German translation
|
|
3076
|
+
rdf-construct localise extract ontology.ttl --language de -o translations/de.yml
|
|
3077
|
+
|
|
3078
|
+
# Merge completed translations
|
|
3079
|
+
rdf-construct localise merge ontology.ttl translations/de.yml -o localised.ttl
|
|
3080
|
+
|
|
3081
|
+
# Check translation coverage
|
|
3082
|
+
rdf-construct localise report ontology.ttl --languages en,de,fr
|
|
3083
|
+
"""
|
|
3084
|
+
pass
|
|
3085
|
+
|
|
3086
|
+
|
|
3087
|
+
@localise.command("extract")
|
|
3088
|
+
@click.argument("source", type=click.Path(exists=True, path_type=Path))
|
|
3089
|
+
@click.option(
|
|
3090
|
+
"--language",
|
|
3091
|
+
"-l",
|
|
3092
|
+
"target_language",
|
|
3093
|
+
required=True,
|
|
3094
|
+
help="Target language code (e.g., de, fr, es)",
|
|
3095
|
+
)
|
|
3096
|
+
@click.option(
|
|
3097
|
+
"--output",
|
|
3098
|
+
"-o",
|
|
3099
|
+
type=click.Path(path_type=Path),
|
|
3100
|
+
help="Output YAML file (default: {language}.yml)",
|
|
3101
|
+
)
|
|
3102
|
+
@click.option(
|
|
3103
|
+
"--source-language",
|
|
3104
|
+
default="en",
|
|
3105
|
+
help="Source language code (default: en)",
|
|
3106
|
+
)
|
|
3107
|
+
@click.option(
|
|
3108
|
+
"--properties",
|
|
3109
|
+
"-p",
|
|
3110
|
+
help="Comma-separated properties to extract (e.g., rdfs:label,rdfs:comment)",
|
|
3111
|
+
)
|
|
3112
|
+
@click.option(
|
|
3113
|
+
"--include-deprecated",
|
|
3114
|
+
is_flag=True,
|
|
3115
|
+
help="Include deprecated entities",
|
|
3116
|
+
)
|
|
3117
|
+
@click.option(
|
|
3118
|
+
"--missing-only",
|
|
3119
|
+
is_flag=True,
|
|
3120
|
+
help="Only extract strings missing in target language",
|
|
3121
|
+
)
|
|
3122
|
+
@click.option(
|
|
3123
|
+
"--config",
|
|
3124
|
+
"-c",
|
|
3125
|
+
"config_file",
|
|
3126
|
+
type=click.Path(exists=True, path_type=Path),
|
|
3127
|
+
help="YAML configuration file",
|
|
3128
|
+
)
|
|
3129
|
+
def localise_extract(
|
|
3130
|
+
source: Path,
|
|
3131
|
+
target_language: str,
|
|
3132
|
+
output: Path | None,
|
|
3133
|
+
source_language: str,
|
|
3134
|
+
properties: str | None,
|
|
3135
|
+
include_deprecated: bool,
|
|
3136
|
+
missing_only: bool,
|
|
3137
|
+
config_file: Path | None,
|
|
3138
|
+
):
|
|
3139
|
+
"""Extract translatable strings from an ontology.
|
|
3140
|
+
|
|
3141
|
+
Generates a YAML file with source text and empty translation fields,
|
|
3142
|
+
ready to be filled in by translators.
|
|
3143
|
+
|
|
3144
|
+
\b
|
|
3145
|
+
Examples:
|
|
3146
|
+
# Basic extraction
|
|
3147
|
+
rdf-construct localise extract ontology.ttl --language de -o de.yml
|
|
3148
|
+
|
|
3149
|
+
# Extract only labels
|
|
3150
|
+
rdf-construct localise extract ontology.ttl -l de -p rdfs:label
|
|
3151
|
+
|
|
3152
|
+
# Extract missing strings only (for updates)
|
|
3153
|
+
rdf-construct localise extract ontology.ttl -l de --missing-only -o de_update.yml
|
|
3154
|
+
"""
|
|
3155
|
+
from rdflib import Graph
|
|
3156
|
+
from rdf_construct.localise import (
|
|
3157
|
+
StringExtractor,
|
|
3158
|
+
ExtractConfig,
|
|
3159
|
+
get_formatter as get_localise_formatter,
|
|
3160
|
+
)
|
|
3161
|
+
|
|
3162
|
+
# Load config if provided
|
|
3163
|
+
if config_file:
|
|
3164
|
+
from rdf_construct.localise import load_localise_config
|
|
3165
|
+
config = load_localise_config(config_file)
|
|
3166
|
+
extract_config = config.extract
|
|
3167
|
+
extract_config.target_language = target_language
|
|
3168
|
+
else:
|
|
3169
|
+
# Build config from CLI args
|
|
3170
|
+
prop_list = None
|
|
3171
|
+
if properties:
|
|
3172
|
+
prop_list = [_expand_localise_property(p.strip()) for p in properties.split(",")]
|
|
3173
|
+
|
|
3174
|
+
extract_config = ExtractConfig(
|
|
3175
|
+
source_language=source_language,
|
|
3176
|
+
target_language=target_language,
|
|
3177
|
+
properties=prop_list or ExtractConfig().properties,
|
|
3178
|
+
include_deprecated=include_deprecated,
|
|
3179
|
+
missing_only=missing_only,
|
|
3180
|
+
)
|
|
3181
|
+
|
|
3182
|
+
# Load graph
|
|
3183
|
+
click.echo(f"Loading {source}...")
|
|
3184
|
+
graph = Graph()
|
|
3185
|
+
graph.parse(source)
|
|
3186
|
+
|
|
3187
|
+
# Extract
|
|
3188
|
+
click.echo(f"Extracting strings for {target_language}...")
|
|
3189
|
+
extractor = StringExtractor(extract_config)
|
|
3190
|
+
result = extractor.extract(graph, source, target_language)
|
|
3191
|
+
|
|
3192
|
+
# Display result
|
|
3193
|
+
formatter = get_localise_formatter("text")
|
|
3194
|
+
click.echo(formatter.format_extraction_result(result))
|
|
3195
|
+
|
|
3196
|
+
if not result.success:
|
|
3197
|
+
raise SystemExit(2)
|
|
3198
|
+
|
|
3199
|
+
# Save output
|
|
3200
|
+
output_path = output or Path(f"{target_language}.yml")
|
|
3201
|
+
if result.translation_file:
|
|
3202
|
+
result.translation_file.save(output_path)
|
|
3203
|
+
click.echo()
|
|
3204
|
+
click.secho(f"✓ Wrote {output_path}", fg="green")
|
|
3205
|
+
|
|
3206
|
+
raise SystemExit(0)
|
|
3207
|
+
|
|
3208
|
+
|
|
3209
|
+
@localise.command("merge")
|
|
3210
|
+
@click.argument("source", type=click.Path(exists=True, path_type=Path))
|
|
3211
|
+
@click.argument("translations", nargs=-1, required=True, type=click.Path(exists=True, path_type=Path))
|
|
3212
|
+
@click.option(
|
|
3213
|
+
"--output",
|
|
3214
|
+
"-o",
|
|
3215
|
+
type=click.Path(path_type=Path),
|
|
3216
|
+
required=True,
|
|
3217
|
+
help="Output file for merged ontology",
|
|
3218
|
+
)
|
|
3219
|
+
@click.option(
|
|
3220
|
+
"--status",
|
|
3221
|
+
type=click.Choice(["pending", "needs_review", "translated", "approved"], case_sensitive=False),
|
|
3222
|
+
default="translated",
|
|
3223
|
+
help="Minimum status to include (default: translated)",
|
|
3224
|
+
)
|
|
3225
|
+
@click.option(
|
|
3226
|
+
"--existing",
|
|
3227
|
+
type=click.Choice(["preserve", "overwrite"], case_sensitive=False),
|
|
3228
|
+
default="preserve",
|
|
3229
|
+
help="How to handle existing translations (default: preserve)",
|
|
3230
|
+
)
|
|
3231
|
+
@click.option(
|
|
3232
|
+
"--dry-run",
|
|
3233
|
+
is_flag=True,
|
|
3234
|
+
help="Show what would happen without writing files",
|
|
3235
|
+
)
|
|
3236
|
+
@click.option(
|
|
3237
|
+
"--no-colour",
|
|
3238
|
+
is_flag=True,
|
|
3239
|
+
help="Disable coloured output",
|
|
3240
|
+
)
|
|
3241
|
+
def localise_merge(
|
|
3242
|
+
source: Path,
|
|
3243
|
+
translations: tuple[Path, ...],
|
|
3244
|
+
output: Path,
|
|
3245
|
+
status: str,
|
|
3246
|
+
existing: str,
|
|
3247
|
+
dry_run: bool,
|
|
3248
|
+
no_colour: bool,
|
|
3249
|
+
):
|
|
3250
|
+
"""Merge translation files back into an ontology.
|
|
3251
|
+
|
|
3252
|
+
Takes completed YAML translation files and adds the translations
|
|
3253
|
+
as new language-tagged literals to the ontology.
|
|
3254
|
+
|
|
3255
|
+
\b
|
|
3256
|
+
Examples:
|
|
3257
|
+
# Merge single translation file
|
|
3258
|
+
rdf-construct localise merge ontology.ttl de.yml -o localised.ttl
|
|
3259
|
+
|
|
3260
|
+
# Merge multiple languages
|
|
3261
|
+
rdf-construct localise merge ontology.ttl translations/*.yml -o multilingual.ttl
|
|
3262
|
+
|
|
3263
|
+
# Merge only approved translations
|
|
3264
|
+
rdf-construct localise merge ontology.ttl de.yml --status approved -o localised.ttl
|
|
3265
|
+
"""
|
|
3266
|
+
from rdflib import Graph
|
|
3267
|
+
from rdf_construct.localise import (
|
|
3268
|
+
TranslationMerger,
|
|
3269
|
+
TranslationFile,
|
|
3270
|
+
MergeConfig as LocaliseMergeConfig,
|
|
3271
|
+
TranslationStatus,
|
|
3272
|
+
ExistingStrategy,
|
|
3273
|
+
get_formatter as get_localise_formatter,
|
|
3274
|
+
)
|
|
3275
|
+
|
|
3276
|
+
# Load graph
|
|
3277
|
+
click.echo(f"Loading {source}...")
|
|
3278
|
+
graph = Graph()
|
|
3279
|
+
graph.parse(source)
|
|
3280
|
+
|
|
3281
|
+
# Load translation files
|
|
3282
|
+
click.echo(f"Loading {len(translations)} translation file(s)...")
|
|
3283
|
+
trans_files = [TranslationFile.from_yaml(p) for p in translations]
|
|
3284
|
+
|
|
3285
|
+
# Build config
|
|
3286
|
+
config = LocaliseMergeConfig(
|
|
3287
|
+
min_status=TranslationStatus(status),
|
|
3288
|
+
existing=ExistingStrategy(existing),
|
|
3289
|
+
)
|
|
3290
|
+
|
|
3291
|
+
# Merge
|
|
3292
|
+
click.echo("Merging translations...")
|
|
3293
|
+
merger = TranslationMerger(config)
|
|
3294
|
+
result = merger.merge_multiple(graph, trans_files)
|
|
3295
|
+
|
|
3296
|
+
# Display result
|
|
3297
|
+
formatter = get_localise_formatter("text", use_colour=not no_colour)
|
|
3298
|
+
click.echo(formatter.format_merge_result(result))
|
|
3299
|
+
|
|
3300
|
+
if not result.success:
|
|
3301
|
+
raise SystemExit(2)
|
|
3302
|
+
|
|
3303
|
+
# Save output (unless dry run)
|
|
3304
|
+
if not dry_run and result.merged_graph:
|
|
3305
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
3306
|
+
result.merged_graph.serialize(destination=output, format="turtle")
|
|
3307
|
+
click.echo()
|
|
3308
|
+
click.secho(f"✓ Wrote {output}", fg="green")
|
|
3309
|
+
|
|
3310
|
+
# Exit code based on warnings
|
|
3311
|
+
if result.stats.errors > 0:
|
|
3312
|
+
raise SystemExit(1)
|
|
3313
|
+
else:
|
|
3314
|
+
raise SystemExit(0)
|
|
3315
|
+
|
|
3316
|
+
|
|
3317
|
+
@localise.command("report")
|
|
3318
|
+
@click.argument("source", type=click.Path(exists=True, path_type=Path))
|
|
3319
|
+
@click.option(
|
|
3320
|
+
"--languages",
|
|
3321
|
+
"-l",
|
|
3322
|
+
required=True,
|
|
3323
|
+
help="Comma-separated language codes to check (e.g., en,de,fr)",
|
|
3324
|
+
)
|
|
3325
|
+
@click.option(
|
|
3326
|
+
"--source-language",
|
|
3327
|
+
default="en",
|
|
3328
|
+
help="Base language for translations (default: en)",
|
|
3329
|
+
)
|
|
3330
|
+
@click.option(
|
|
3331
|
+
"--properties",
|
|
3332
|
+
"-p",
|
|
3333
|
+
help="Comma-separated properties to check",
|
|
3334
|
+
)
|
|
3335
|
+
@click.option(
|
|
3336
|
+
"--output",
|
|
3337
|
+
"-o",
|
|
3338
|
+
type=click.Path(path_type=Path),
|
|
3339
|
+
help="Output file for report",
|
|
3340
|
+
)
|
|
3341
|
+
@click.option(
|
|
3342
|
+
"--format",
|
|
3343
|
+
"-f",
|
|
3344
|
+
"output_format",
|
|
3345
|
+
type=click.Choice(["text", "markdown", "md"], case_sensitive=False),
|
|
3346
|
+
default="text",
|
|
3347
|
+
help="Output format (default: text)",
|
|
3348
|
+
)
|
|
3349
|
+
@click.option(
|
|
3350
|
+
"--verbose",
|
|
3351
|
+
"-v",
|
|
3352
|
+
is_flag=True,
|
|
3353
|
+
help="Show detailed missing translation list",
|
|
3354
|
+
)
|
|
3355
|
+
@click.option(
|
|
3356
|
+
"--no-colour",
|
|
3357
|
+
is_flag=True,
|
|
3358
|
+
help="Disable coloured output",
|
|
3359
|
+
)
|
|
3360
|
+
def localise_report(
|
|
3361
|
+
source: Path,
|
|
3362
|
+
languages: str,
|
|
3363
|
+
source_language: str,
|
|
3364
|
+
properties: str | None,
|
|
3365
|
+
output: Path | None,
|
|
3366
|
+
output_format: str,
|
|
3367
|
+
verbose: bool,
|
|
3368
|
+
no_colour: bool,
|
|
3369
|
+
):
|
|
3370
|
+
"""Generate translation coverage report.
|
|
3371
|
+
|
|
3372
|
+
Analyses an ontology and reports what percentage of translatable
|
|
3373
|
+
content has been translated into each target language.
|
|
3374
|
+
|
|
3375
|
+
\b
|
|
3376
|
+
Examples:
|
|
3377
|
+
# Basic coverage report
|
|
3378
|
+
rdf-construct localise report ontology.ttl --languages en,de,fr
|
|
3379
|
+
|
|
3380
|
+
# Detailed report with missing entities
|
|
3381
|
+
rdf-construct localise report ontology.ttl -l en,de,fr --verbose
|
|
3382
|
+
|
|
3383
|
+
# Markdown report to file
|
|
3384
|
+
rdf-construct localise report ontology.ttl -l en,de,fr -f markdown -o coverage.md
|
|
3385
|
+
"""
|
|
3386
|
+
from rdflib import Graph
|
|
3387
|
+
from rdf_construct.localise import (
|
|
3388
|
+
CoverageReporter,
|
|
3389
|
+
get_formatter as get_localise_formatter,
|
|
3390
|
+
)
|
|
3391
|
+
|
|
3392
|
+
# Parse languages
|
|
3393
|
+
lang_list = [lang.strip() for lang in languages.split(",")]
|
|
3394
|
+
|
|
3395
|
+
# Parse properties
|
|
3396
|
+
prop_list = None
|
|
3397
|
+
if properties:
|
|
3398
|
+
prop_list = [_expand_localise_property(p.strip()) for p in properties.split(",")]
|
|
3399
|
+
|
|
3400
|
+
# Load graph
|
|
3401
|
+
click.echo(f"Loading {source}...")
|
|
3402
|
+
graph = Graph()
|
|
3403
|
+
graph.parse(source)
|
|
3404
|
+
|
|
3405
|
+
# Generate report
|
|
3406
|
+
click.echo("Analysing translation coverage...")
|
|
3407
|
+
reporter = CoverageReporter(
|
|
3408
|
+
source_language=source_language,
|
|
3409
|
+
properties=prop_list,
|
|
3410
|
+
)
|
|
3411
|
+
report = reporter.report(graph, lang_list, source)
|
|
3412
|
+
|
|
3413
|
+
# Format output
|
|
3414
|
+
formatter = get_localise_formatter(output_format, use_colour=not no_colour)
|
|
3415
|
+
report_text = formatter.format_coverage_report(report, verbose=verbose)
|
|
3416
|
+
|
|
3417
|
+
# Output
|
|
3418
|
+
if output:
|
|
3419
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
3420
|
+
output.write_text(report_text)
|
|
3421
|
+
click.secho(f"✓ Wrote {output}", fg="green")
|
|
3422
|
+
else:
|
|
3423
|
+
click.echo()
|
|
3424
|
+
click.echo(report_text)
|
|
3425
|
+
|
|
3426
|
+
raise SystemExit(0)
|
|
3427
|
+
|
|
3428
|
+
|
|
3429
|
+
@localise.command("init")
|
|
3430
|
+
@click.argument("source", type=click.Path(exists=True, path_type=Path))
|
|
3431
|
+
@click.option(
|
|
3432
|
+
"--language",
|
|
3433
|
+
"-l",
|
|
3434
|
+
"target_language",
|
|
3435
|
+
required=True,
|
|
3436
|
+
help="Target language code (e.g., de, fr, es)",
|
|
3437
|
+
)
|
|
3438
|
+
@click.option(
|
|
3439
|
+
"--output",
|
|
3440
|
+
"-o",
|
|
3441
|
+
type=click.Path(path_type=Path),
|
|
3442
|
+
help="Output YAML file (default: {language}.yml)",
|
|
3443
|
+
)
|
|
3444
|
+
@click.option(
|
|
3445
|
+
"--source-language",
|
|
3446
|
+
default="en",
|
|
3447
|
+
help="Source language code (default: en)",
|
|
3448
|
+
)
|
|
3449
|
+
def localise_init(
|
|
3450
|
+
source: Path,
|
|
3451
|
+
target_language: str,
|
|
3452
|
+
output: Path | None,
|
|
3453
|
+
source_language: str,
|
|
3454
|
+
):
|
|
3455
|
+
"""Create empty translation file for a new language.
|
|
3456
|
+
|
|
3457
|
+
Equivalent to 'extract' but explicitly for starting a new language.
|
|
3458
|
+
|
|
3459
|
+
\b
|
|
3460
|
+
Examples:
|
|
3461
|
+
rdf-construct localise init ontology.ttl --language ja -o translations/ja.yml
|
|
3462
|
+
"""
|
|
3463
|
+
from rdflib import Graph
|
|
3464
|
+
from rdf_construct.localise import (
|
|
3465
|
+
StringExtractor,
|
|
3466
|
+
ExtractConfig,
|
|
3467
|
+
get_formatter as get_localise_formatter,
|
|
3468
|
+
)
|
|
3469
|
+
|
|
3470
|
+
# Build config
|
|
3471
|
+
extract_config = ExtractConfig(
|
|
3472
|
+
source_language=source_language,
|
|
3473
|
+
target_language=target_language,
|
|
3474
|
+
)
|
|
3475
|
+
|
|
3476
|
+
# Load graph
|
|
3477
|
+
click.echo(f"Loading {source}...")
|
|
3478
|
+
graph = Graph()
|
|
3479
|
+
graph.parse(source)
|
|
3480
|
+
|
|
3481
|
+
# Extract
|
|
3482
|
+
click.echo(f"Initialising translation file for {target_language}...")
|
|
3483
|
+
extractor = StringExtractor(extract_config)
|
|
3484
|
+
result = extractor.extract(graph, source, target_language)
|
|
3485
|
+
|
|
3486
|
+
# Display result
|
|
3487
|
+
formatter = get_localise_formatter("text")
|
|
3488
|
+
click.echo(formatter.format_extraction_result(result))
|
|
3489
|
+
|
|
3490
|
+
if not result.success:
|
|
3491
|
+
raise SystemExit(2)
|
|
3492
|
+
|
|
3493
|
+
# Save output
|
|
3494
|
+
output_path = output or Path(f"{target_language}.yml")
|
|
3495
|
+
if result.translation_file:
|
|
3496
|
+
result.translation_file.save(output_path)
|
|
3497
|
+
click.echo()
|
|
3498
|
+
click.secho(f"✓ Created {output_path}", fg="green")
|
|
3499
|
+
click.echo(f" Fill in translations and run:")
|
|
3500
|
+
click.echo(f" rdf-construct localise merge {source} {output_path} -o localised.ttl")
|
|
3501
|
+
|
|
3502
|
+
raise SystemExit(0)
|
|
3503
|
+
|
|
3504
|
+
|
|
3505
|
+
@localise.command("config")
|
|
3506
|
+
@click.option(
|
|
3507
|
+
"--init",
|
|
3508
|
+
"init_config",
|
|
3509
|
+
is_flag=True,
|
|
3510
|
+
help="Generate a default localise configuration file",
|
|
3511
|
+
)
|
|
3512
|
+
def localise_config(init_config: bool):
|
|
3513
|
+
"""Generate or validate localise configuration.
|
|
3514
|
+
|
|
3515
|
+
\b
|
|
3516
|
+
Examples:
|
|
3517
|
+
rdf-construct localise config --init
|
|
3518
|
+
"""
|
|
3519
|
+
from rdf_construct.localise import create_default_config as create_default_localise_config
|
|
3520
|
+
|
|
3521
|
+
if init_config:
|
|
3522
|
+
config_path = Path("localise.yml")
|
|
3523
|
+
if config_path.exists():
|
|
3524
|
+
click.secho(f"Config file already exists: {config_path}", fg="red", err=True)
|
|
3525
|
+
raise click.Abort()
|
|
3526
|
+
|
|
3527
|
+
config_content = create_default_localise_config()
|
|
3528
|
+
config_path.write_text(config_content)
|
|
3529
|
+
click.secho(f"Created {config_path}", fg="green")
|
|
3530
|
+
click.echo("Edit this file to configure your localisation workflow.")
|
|
3531
|
+
else:
|
|
3532
|
+
click.echo("Use --init to create a default configuration file.")
|
|
3533
|
+
|
|
3534
|
+
raise SystemExit(0)
|
|
3535
|
+
|
|
3536
|
+
|
|
3537
|
+
def _expand_localise_property(prop: str) -> str:
|
|
3538
|
+
"""Expand a CURIE to full URI for localise commands."""
|
|
3539
|
+
prefixes = {
|
|
3540
|
+
"rdfs:": "http://www.w3.org/2000/01/rdf-schema#",
|
|
3541
|
+
"skos:": "http://www.w3.org/2004/02/skos/core#",
|
|
3542
|
+
"owl:": "http://www.w3.org/2002/07/owl#",
|
|
3543
|
+
"rdf:": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
|
3544
|
+
"dc:": "http://purl.org/dc/elements/1.1/",
|
|
3545
|
+
"dcterms:": "http://purl.org/dc/terms/",
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
for prefix, namespace in prefixes.items():
|
|
3549
|
+
if prop.startswith(prefix):
|
|
3550
|
+
return namespace + prop[len(prefix):]
|
|
3551
|
+
|
|
3552
|
+
return prop
|
|
3553
|
+
|
|
3554
|
+
|
|
1761
3555
|
if __name__ == "__main__":
|
|
1762
3556
|
cli()
|