nativegate 0.1.2__tar.gz → 0.1.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nativegate-0.1.2 → nativegate-0.1.4}/PKG-INFO +1 -1
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/cli.py +86 -4
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/config.py +31 -0
- nativegate-0.1.4/nativegate/parsers/dialect.py +98 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate.egg-info/PKG-INFO +1 -1
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate.egg-info/SOURCES.txt +3 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/pyproject.toml +1 -1
- nativegate-0.1.4/tests/test_config_unknown_keys.py +77 -0
- nativegate-0.1.4/tests/test_dialect.py +184 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/README.md +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/__init__.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/__main__.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/buildinfo.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/declared_invariants.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/discovery.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/driverbuild.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/drivers/__init__.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/drivers/cpp.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/drivers/fortran.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/__init__.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/cmake_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/docker_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/error_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/f2py_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/gateway_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/golden_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/k8s_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/mcp_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/middleware_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/pybind_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/pyproject_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/python_pkg_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/generators/test_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/golden.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/invariants.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/ir.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/lattice.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/locking.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/oracle.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/__init__.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/cpp.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/cpp_ast.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/cpp_regex.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/fixed_form.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/fortran.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/fortran_fparser.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/parsers/fortran_regex.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/preprocess.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/structural_invariants.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/suggest.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/templates/__init__.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/templates/golden_test_template.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate/wire.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate.egg-info/dependency_links.txt +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate.egg-info/entry_points.txt +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate.egg-info/requires.txt +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/nativegate.egg-info/top_level.txt +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/setup.cfg +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_buildinfo.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cli_serve_verify.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cli_suggest.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cli_verify_aggregate.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cmake_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cpp_ast.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cpp_driver.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_cpp_pipeline.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_declared_invariants.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_derived_types.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_docker_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_driverbuild.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_endpoint_docstrings.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_fixed_form.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_fortran_doc_comments.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_fortran_driver.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_fortran_fparser.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_fortran_libraries.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_fortran_pipeline.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_gateway_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_generated_python_is_valid.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_golden.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_invariants.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_ir_contract.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_k8s_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_lattice.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_legacy_cpp.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_legacy_fortran.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_locking.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_mcp_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_middleware_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_multifile_cpp.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_native_oracle.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_native_state_contract.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_oracle.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_preprocess_and_config.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_preprocessed_fortran.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_pybind_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_pyproject_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_quickstart.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_scalar_ref.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_service_gen.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_service_name_validation.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_shared_libraries.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_structural_invariants.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_suggest.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_verification_config.py +0 -0
- {nativegate-0.1.2 → nativegate-0.1.4}/tests/test_wire.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nativegate
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Make legacy C++/C/Fortran AI-ready and microservice-ready: expose native code as an MCP server, REST API and container
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -69,6 +69,7 @@ from .parsers import cpp as cpp_parser
|
|
|
69
69
|
from .parsers.cpp_ast import ClangOptions
|
|
70
70
|
from .parsers import fixed_form
|
|
71
71
|
from .parsers import fortran as fortran_parser
|
|
72
|
+
from .parsers.dialect import apply_dialect, is_dialect_marked, resolve_dialect
|
|
72
73
|
|
|
73
74
|
SERVICES_DIR = "services"
|
|
74
75
|
|
|
@@ -244,7 +245,15 @@ def create_service(name: str, language: str, force: bool) -> None:
|
|
|
244
245
|
help="Delete and recreate services/<name> if it already exists.",
|
|
245
246
|
)
|
|
246
247
|
@click.option("--build/--no-build", default=False, help="Also compile the extension after generating.")
|
|
247
|
-
|
|
248
|
+
@click.option(
|
|
249
|
+
"--dialect",
|
|
250
|
+
default="",
|
|
251
|
+
help="Select the live half of netlib-style dual-dialect sources "
|
|
252
|
+
"(CS/CD machine-precision marking): 'cs' or 'cd'. The resolved copy "
|
|
253
|
+
"goes to native/_expanded; the source under native/ is untouched. "
|
|
254
|
+
"Leave empty for sources that are not dialect-marked.",
|
|
255
|
+
)
|
|
256
|
+
def quickstart(source: Path, name: str | None, force: bool, build: bool, dialect: str) -> None:
|
|
248
257
|
"""One-shot: scaffold a service from a single C++/Fortran file, expose
|
|
249
258
|
everything found in it, and generate the Python package — no manual
|
|
250
259
|
nativegate.yaml editing.
|
|
@@ -310,10 +319,51 @@ def quickstart(source: Path, name: str | None, force: bool, build: bool) -> None
|
|
|
310
319
|
f"{service_dir / 'native'} and re-run `ngate generate {service_name}`."
|
|
311
320
|
)
|
|
312
321
|
else:
|
|
313
|
-
|
|
322
|
+
resolved = native_copy
|
|
323
|
+
try:
|
|
324
|
+
config.dialect = resolve_dialect(dialect)
|
|
325
|
+
except ValueError as exc:
|
|
326
|
+
tracker.fail("Copy native source")
|
|
327
|
+
raise click.ClickException(str(exc)) from exc
|
|
328
|
+
if config.dialect and is_dialect_marked(read_source(native_copy)):
|
|
329
|
+
resolved_dir = service_dir / "native" / "_expanded"
|
|
330
|
+
resolved_dir.mkdir(parents=True, exist_ok=True)
|
|
331
|
+
resolved = resolved_dir / native_copy.name
|
|
332
|
+
resolved.write_text(apply_dialect(read_source(native_copy), config.dialect))
|
|
333
|
+
tracker.log(
|
|
334
|
+
f"dialect '{config.dialect}' selected for {native_copy.name} "
|
|
335
|
+
f"-> {resolved} (native/ keeps the untouched upstream bytes)"
|
|
336
|
+
)
|
|
337
|
+
else:
|
|
338
|
+
config.dialect = ""
|
|
339
|
+
try:
|
|
340
|
+
routines = fortran_parser.list_routine_names(resolved)
|
|
341
|
+
except ValueError as exc:
|
|
342
|
+
# Why not pass the parser's message through untouched: for a
|
|
343
|
+
# netlib CS/CD dual-dialect deck the parser error ("floating
|
|
344
|
+
# continuation") is real but does not name the fix, which is
|
|
345
|
+
# one flag on this very command.
|
|
346
|
+
hint = ""
|
|
347
|
+
if not config.dialect and is_dialect_marked(read_source(native_copy)):
|
|
348
|
+
hint = (
|
|
349
|
+
"\nThis file looks dialect-marked (CS/CD prefix on both "
|
|
350
|
+
"a single- and a double-precision copy of every line). "
|
|
351
|
+
"Pass '--dialect cs' or '--dialect cd' to select the "
|
|
352
|
+
"live half."
|
|
353
|
+
)
|
|
354
|
+
tracker.fail("Copy native source")
|
|
355
|
+
raise click.ClickException(f"{exc}{hint}") from exc
|
|
314
356
|
if not routines:
|
|
315
357
|
tracker.fail("Copy native source")
|
|
316
|
-
|
|
358
|
+
hint = ""
|
|
359
|
+
if not config.dialect and is_dialect_marked(read_source(native_copy)):
|
|
360
|
+
hint = (
|
|
361
|
+
" (The source carries netlib CS/CD dual-dialect marking: "
|
|
362
|
+
"pass --dialect cs or --dialect cd to select the live half.)"
|
|
363
|
+
)
|
|
364
|
+
raise click.ClickException(
|
|
365
|
+
f"No Fortran function/subroutine declarations found in {source}.{hint}"
|
|
366
|
+
)
|
|
317
367
|
config.expose.functions = routines
|
|
318
368
|
config.save(service_dir)
|
|
319
369
|
tracker.log(f"Exposing Fortran routines: {', '.join(routines)}")
|
|
@@ -1719,6 +1769,35 @@ def _generate_fortran_service(service_dir: Path, config: ServiceConfig) -> None:
|
|
|
1719
1769
|
if not sources:
|
|
1720
1770
|
raise click.ClickException(f"No Fortran sources found under {service_dir / 'native'}.")
|
|
1721
1771
|
|
|
1772
|
+
# netlib dual-dialect sources (CS/CD stamped on both halves' every line)
|
|
1773
|
+
# are not parseable by anything that reads real Fortran — see
|
|
1774
|
+
# DEFECTS D10. The selected dialect's lines become the code, the other
|
|
1775
|
+
# becomes a comment, and the resolved copy lives in `_expanded` next
|
|
1776
|
+
# to the other generated transforms. Done at the very front, so every
|
|
1777
|
+
# reader of a source (routine discovery, the parse, intent inference,
|
|
1778
|
+
# f2py) sees the same resolved file.
|
|
1779
|
+
dialect_set = set()
|
|
1780
|
+
if config.dialect:
|
|
1781
|
+
chosen = [s for s in sources if is_dialect_marked(s.read_text())]
|
|
1782
|
+
expanded_dir = service_dir / "native" / "_expanded"
|
|
1783
|
+
expanded_dir.mkdir(parents=True, exist_ok=True)
|
|
1784
|
+
replaced: list[Path] = []
|
|
1785
|
+
for source in sources:
|
|
1786
|
+
if source not in chosen:
|
|
1787
|
+
replaced.append(source)
|
|
1788
|
+
continue
|
|
1789
|
+
target = expanded_dir / source.name
|
|
1790
|
+
target.write_text(apply_dialect(read_source(source), config.dialect))
|
|
1791
|
+
replaced.append(target)
|
|
1792
|
+
dialect_set.add(target)
|
|
1793
|
+
sources = replaced
|
|
1794
|
+
if chosen:
|
|
1795
|
+
click.echo(
|
|
1796
|
+
f"Resolved dialect '{config.dialect}' in {len(chosen)} source(s) "
|
|
1797
|
+
f"-> native/_expanded. The '{config.dialect}' half is now live; "
|
|
1798
|
+
"the other half is commented. Originals under native/ are untouched."
|
|
1799
|
+
)
|
|
1800
|
+
|
|
1722
1801
|
include_paths = [Path(p) for p in config.include_paths]
|
|
1723
1802
|
for p in include_paths:
|
|
1724
1803
|
if not p.is_dir():
|
|
@@ -1910,13 +1989,16 @@ def _generate_fortran_service(service_dir: Path, config: ServiceConfig) -> None:
|
|
|
1910
1989
|
+ kind_param_sources
|
|
1911
1990
|
+ shim_only_sources
|
|
1912
1991
|
)
|
|
1992
|
+
# Dialect-resolved copies live under _expanded by construction, so the
|
|
1993
|
+
# referenced tree must be built even when nothing else needs rewriting.
|
|
1994
|
+
rewritten_or_copied = bool(rewritten) or bool(library_sources) or bool(preprocessed_set) or bool(dialect_set)
|
|
1913
1995
|
# A library source is always copied under the service, even when it needs
|
|
1914
1996
|
# no rewriting: it lives outside native/, so referencing it as
|
|
1915
1997
|
# `native/<name>` would name a file that is not there — and the Docker
|
|
1916
1998
|
# build context is the service directory, so a path outside it could not be
|
|
1917
1999
|
# COPYed in anyway.
|
|
1918
2000
|
library_set = set(library_sources)
|
|
1919
|
-
if
|
|
2001
|
+
if rewritten_or_copied:
|
|
1920
2002
|
expanded_dir = service_dir / "native" / "_expanded"
|
|
1921
2003
|
expanded_dir.mkdir(parents=True, exist_ok=True)
|
|
1922
2004
|
native_sources = []
|
|
@@ -10,6 +10,8 @@ from typing import TYPE_CHECKING, Union
|
|
|
10
10
|
|
|
11
11
|
import yaml
|
|
12
12
|
|
|
13
|
+
from .parsers.dialect import resolve_dialect
|
|
14
|
+
|
|
13
15
|
if TYPE_CHECKING:
|
|
14
16
|
from .ir import ModuleIR
|
|
15
17
|
|
|
@@ -783,6 +785,12 @@ class ServiceConfig:
|
|
|
783
785
|
# the `_expanded` copy. Same reason clang.defines exists for C++: the
|
|
784
786
|
# branches differ, and only the build knows which one is live.
|
|
785
787
|
fortran_defines: list[str] = field(default_factory=list)
|
|
788
|
+
# Dialect selection for netlib-style dual-dialect sources ("cs" or
|
|
789
|
+
# "cd"), applied when a file is parsed AND before f2py compiles it.
|
|
790
|
+
# Empty means no source carries the marking; discriminating per-file
|
|
791
|
+
# is done by `is_dialect_marked`, so one value safely covers a mixed
|
|
792
|
+
# tree.
|
|
793
|
+
dialect: str = ""
|
|
786
794
|
# `state:`/`invariants:`/`ranges:` — layer 3 declarations
|
|
787
795
|
# (design-verification-layers.md §3.2-3.5). Optional: a service with no
|
|
788
796
|
# invariants.json coverage simply has an empty VerificationConfig.
|
|
@@ -797,6 +805,26 @@ class ServiceConfig:
|
|
|
797
805
|
"Run `ngate create-service` first."
|
|
798
806
|
)
|
|
799
807
|
data = yaml.safe_load(config_path.read_text()) or {}
|
|
808
|
+
# Unknown top-level keys are an error, not dead weight. A new
|
|
809
|
+
# nativegate.yaml key read by an older nativegate (the exact case
|
|
810
|
+
# this project shipped CI against: `dialect: cd` under a nativegate
|
|
811
|
+
# that pre-dated it) would otherwise vanish the same way a letter
|
|
812
|
+
# transposed in `dialete:` would — the service generates from an
|
|
813
|
+
# assumption the user never wrote.
|
|
814
|
+
known_keys = {
|
|
815
|
+
"name", "language", "expose", "parser", "clang", "libraries",
|
|
816
|
+
"include_paths", "api", "dialect",
|
|
817
|
+
"fortran", # namespace for fortran.defines
|
|
818
|
+
# verification layer 3 keys
|
|
819
|
+
"state", "invariants", "ranges", "lattice",
|
|
820
|
+
}
|
|
821
|
+
unknown = sorted(set(data) - known_keys)
|
|
822
|
+
if unknown:
|
|
823
|
+
raise ConfigError(
|
|
824
|
+
f"{config_path}: unknown key(s) {', '.join(unknown)}. "
|
|
825
|
+
"Check the spelling against this nativegate version's supported "
|
|
826
|
+
"keys, or upgrade nativegate if the key is new."
|
|
827
|
+
)
|
|
800
828
|
expose = _load_expose(data.get("expose"), config_path)
|
|
801
829
|
clang_data = data.get("clang") or {}
|
|
802
830
|
# `None` means "no restriction": `expose: all` or an empty `expose:`
|
|
@@ -819,6 +847,7 @@ class ServiceConfig:
|
|
|
819
847
|
),
|
|
820
848
|
libraries=list(data.get("libraries") or []),
|
|
821
849
|
include_paths=list(data.get("include_paths") or []),
|
|
850
|
+
dialect=resolve_dialect(data.get("dialect")),
|
|
822
851
|
api=_load_api(data.get("api"), config_path),
|
|
823
852
|
fortran_defines=list((data.get("fortran") or {}).get("defines") or []),
|
|
824
853
|
verification=_load_verification(data, config_path, known_functions),
|
|
@@ -853,6 +882,8 @@ class ServiceConfig:
|
|
|
853
882
|
data["libraries"] = self.libraries
|
|
854
883
|
if self.include_paths:
|
|
855
884
|
data["include_paths"] = self.include_paths
|
|
885
|
+
if self.dialect:
|
|
886
|
+
data["dialect"] = self.dialect
|
|
856
887
|
# Only written when it differs from the default, so an existing
|
|
857
888
|
# nativegate.yaml does not grow a key that says nothing.
|
|
858
889
|
if self.api.auth != "none":
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Dialect selection for netlib-style dual-dialect Fortran (DEFECTS D10).
|
|
2
|
+
|
|
3
|
+
Some netlib-era sources (specfun 2.5 is the canonical case, 1996) mark
|
|
4
|
+
machine precision by commenting EVERY statement in both dialects and
|
|
5
|
+
asking the installer to comment one batch out manually:
|
|
6
|
+
|
|
7
|
+
CS REAL FUNCTION GAMMA(X)
|
|
8
|
+
CD DOUBLE PRECISION FUNCTION DGAMMA(X)
|
|
9
|
+
CS REAL
|
|
10
|
+
CD DOUBLE PRECISION
|
|
11
|
+
1 C,CONV,EPS, ... <- no statement starts in column 7
|
|
12
|
+
|
|
13
|
+
With neither prefix commented, no standard parser can read the file: the
|
|
14
|
+
grammar has a floating continuation with no statement behind it, and
|
|
15
|
+
gfortran rejects it too ("no implicit type at 'CS'..." paired with the
|
|
16
|
+
dangling continuation). The `NATIVEGATE_FORTRAN_PARSER=regex` fallback
|
|
17
|
+
does not help — the FUNCTION statements are marked the same way.
|
|
18
|
+
|
|
19
|
+
`apply_dialect` makes the choice static in place: the selected prefix is
|
|
20
|
+
uncommented (its two characters become blank columns) and the other
|
|
21
|
+
prefix is pushed under a comment (`C` + whatever remained). Column
|
|
22
|
+
positions are preserved, which is what fixed-form requires: `CS 1 `
|
|
23
|
+
(col 3-6 blank, 1 in column 6) and `CD` lines follow the same geometry.
|
|
24
|
+
|
|
25
|
+
Idempotent: after one pass no line starts CS/CD any more, so re-running
|
|
26
|
+
on an already-resolved file is a no-op, which is what makes it safe for
|
|
27
|
+
generate to apply it to every source without knowing which ones are
|
|
28
|
+
marked.
|
|
29
|
+
|
|
30
|
+
The transform is deliberately pure text on the fixed-form column grid:
|
|
31
|
+
uncommenting replaces the two marker characters with two blanks, so no
|
|
32
|
+
column ever shifts, and commenting restores the `C` in column 1. Only
|
|
33
|
+
`native/_expanded/` carries the resolved file — the byte-for-byte copy
|
|
34
|
+
under `native/native/...` is untouched, so provenance (`golden.json`
|
|
35
|
+
hashes) keeps describing the upstream source, not the transform output.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import re
|
|
41
|
+
|
|
42
|
+
DIALECT_CHOICES = ("cs", "cd")
|
|
43
|
+
|
|
44
|
+
_PREFIX_RE = re.compile(r"^(CS|CD)(?=[ \t])", re.IGNORECASE | re.MULTILINE)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def resolve_dialect(raw: str | None) -> str:
|
|
48
|
+
"""Validate a `dialect:` value from nativegate.yaml / --dialect.
|
|
49
|
+
|
|
50
|
+
`""` means "the source is not dialect-marked, leave everything alone".
|
|
51
|
+
Anything unlisted is an error, not a silent no-op — a typo'd
|
|
52
|
+
`dialect: dc` must fail here, not turn into "unresolved source".
|
|
53
|
+
"""
|
|
54
|
+
value = (raw or "").strip().lower()
|
|
55
|
+
if value and value not in DIALECT_CHOICES:
|
|
56
|
+
raise ValueError(
|
|
57
|
+
f"Unknown dialect '{value}'; expected one of: "
|
|
58
|
+
+ ", ".join(DIALECT_CHOICES) + " (or empty for none)."
|
|
59
|
+
)
|
|
60
|
+
return value.lower()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def is_dialect_marked(text: str) -> bool:
|
|
64
|
+
"""True when any expected prefix appears at a statement column.
|
|
65
|
+
|
|
66
|
+
Deliberately coarse: a file with the markers gets resolved from the
|
|
67
|
+
start, whether or not every line carries them. A single CS/CD word
|
|
68
|
+
that opens a line is a genuine dialect marker in practice — no Fortran
|
|
69
|
+
statement begins with those letters in column 1.
|
|
70
|
+
"""
|
|
71
|
+
return bool(_PREFIX_RE.search(text))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def apply_dialect(text: str, dialect: str) -> str:
|
|
75
|
+
"""Resolve the dual-dialect marking to a single live dialect.
|
|
76
|
+
|
|
77
|
+
`dialect` is the prefix that becomes the code; the other becomes a
|
|
78
|
+
comment (`C ` + the remainder), so the rejected variant stays readable
|
|
79
|
+
in place — the same shape the files will have after a human editor
|
|
80
|
+
does the step netlib's readme prescribes.
|
|
81
|
+
"""
|
|
82
|
+
dialect = resolve_dialect(dialect)
|
|
83
|
+
if not dialect or not is_dialect_marked(text):
|
|
84
|
+
return text
|
|
85
|
+
|
|
86
|
+
selected = dialect.upper()
|
|
87
|
+
out = []
|
|
88
|
+
for line in text.splitlines(keepends=True):
|
|
89
|
+
match = _PREFIX_RE.match(line)
|
|
90
|
+
if not match:
|
|
91
|
+
out.append(line)
|
|
92
|
+
continue
|
|
93
|
+
marker = match.group(1).upper()
|
|
94
|
+
if marker == selected:
|
|
95
|
+
out.append(" " + line[2:])
|
|
96
|
+
else:
|
|
97
|
+
out.append("C " + line[2:])
|
|
98
|
+
return "".join(out)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nativegate
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Make legacy C++/C/Fortran AI-ready and microservice-ready: expose native code as an MCP server, REST API and container
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -45,6 +45,7 @@ nativegate/parsers/__init__.py
|
|
|
45
45
|
nativegate/parsers/cpp.py
|
|
46
46
|
nativegate/parsers/cpp_ast.py
|
|
47
47
|
nativegate/parsers/cpp_regex.py
|
|
48
|
+
nativegate/parsers/dialect.py
|
|
48
49
|
nativegate/parsers/fixed_form.py
|
|
49
50
|
nativegate/parsers/fortran.py
|
|
50
51
|
nativegate/parsers/fortran_fparser.py
|
|
@@ -56,11 +57,13 @@ tests/test_cli_serve_verify.py
|
|
|
56
57
|
tests/test_cli_suggest.py
|
|
57
58
|
tests/test_cli_verify_aggregate.py
|
|
58
59
|
tests/test_cmake_gen.py
|
|
60
|
+
tests/test_config_unknown_keys.py
|
|
59
61
|
tests/test_cpp_ast.py
|
|
60
62
|
tests/test_cpp_driver.py
|
|
61
63
|
tests/test_cpp_pipeline.py
|
|
62
64
|
tests/test_declared_invariants.py
|
|
63
65
|
tests/test_derived_types.py
|
|
66
|
+
tests/test_dialect.py
|
|
64
67
|
tests/test_docker_gen.py
|
|
65
68
|
tests/test_driverbuild.py
|
|
66
69
|
tests/test_endpoint_docstrings.py
|
|
@@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta"
|
|
|
13
13
|
|
|
14
14
|
[project]
|
|
15
15
|
name = "nativegate"
|
|
16
|
-
version = "0.1.
|
|
16
|
+
version = "0.1.4"
|
|
17
17
|
description = "Make legacy C++/C/Fortran AI-ready and microservice-ready: expose native code as an MCP server, REST API and container"
|
|
18
18
|
readme = "README.md"
|
|
19
19
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Unknown top-level keys in nativegate.yaml must fail loading.
|
|
2
|
+
|
|
3
|
+
Found in the wild (2026-09-23): specfun-py's CI installed a nativegate
|
|
4
|
+
that pre-dated the `dialect:` key and generated the service anyway from
|
|
5
|
+
an assumption never written down — the unknown key was swallowed as dead
|
|
6
|
+
weight. A typo in any key was equally silent. Now both are config errors
|
|
7
|
+
that say what is wrong and what to do about it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import shutil
|
|
11
|
+
import textwrap
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from nativegate.config import ConfigError, ServiceConfig
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture()
|
|
19
|
+
def scratch(tmp_path, monkeypatch):
|
|
20
|
+
service = tmp_path / "demo"
|
|
21
|
+
(service / "native").mkdir(parents=True)
|
|
22
|
+
monkeypatch.chdir(tmp_path)
|
|
23
|
+
return service
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _config_for_lines(text):
|
|
27
|
+
def _load(service):
|
|
28
|
+
(service / "nativegate.yaml").write_text(text)
|
|
29
|
+
return ServiceConfig.load(service)
|
|
30
|
+
return _load
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_unknown_key_is_an_error(scratch):
|
|
34
|
+
(scratch / "nativegate.yaml").write_text(textwrap.dedent("""\
|
|
35
|
+
name: demo
|
|
36
|
+
language: fortran
|
|
37
|
+
diatect: cd
|
|
38
|
+
expose:
|
|
39
|
+
functions: []
|
|
40
|
+
"""))
|
|
41
|
+
with pytest.raises(ConfigError) as exc:
|
|
42
|
+
ServiceConfig.load(scratch)
|
|
43
|
+
assert "diatect" in str(exc.value)
|
|
44
|
+
assert "upgrade nativegate" in str(exc.value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_future_key_is_a_config_error_not_a_silent_noop(scratch):
|
|
48
|
+
(scratch / "nativegate.yaml").write_text(textwrap.dedent("""\
|
|
49
|
+
name: demo
|
|
50
|
+
language: fortran
|
|
51
|
+
dialect: cd
|
|
52
|
+
expose:
|
|
53
|
+
functions: []
|
|
54
|
+
"""))
|
|
55
|
+
# fine on a nativegate that knows the key
|
|
56
|
+
config = ServiceConfig.load(scratch)
|
|
57
|
+
assert config.dialect == "cd"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_all_declared_keys_still_load(scratch):
|
|
61
|
+
(scratch / "nativegate.yaml").write_text(textwrap.dedent("""\
|
|
62
|
+
name: demo
|
|
63
|
+
language: fortran
|
|
64
|
+
dialect: cd
|
|
65
|
+
parser: auto
|
|
66
|
+
libraries:
|
|
67
|
+
- petro
|
|
68
|
+
include_paths: []
|
|
69
|
+
fortran:
|
|
70
|
+
defines: []
|
|
71
|
+
expose:
|
|
72
|
+
all: true
|
|
73
|
+
"""))
|
|
74
|
+
config = ServiceConfig.load(scratch)
|
|
75
|
+
assert config.dialect == "cd"
|
|
76
|
+
assert config.libraries == ["petro"]
|
|
77
|
+
assert config.expose.all is True
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""D10: the netlib CS/CD dual-dialect marking.
|
|
2
|
+
|
|
3
|
+
Sources fetched straight from netlib specfun carry every statement twice,
|
|
4
|
+
in both dialects, comment-marked by prefix (CS = single, CD = double), and
|
|
5
|
+
the installer comments one batch out by hand. Neither parser could read
|
|
6
|
+
such a file — the fixed-form grammar sees a continuation with no statement,
|
|
7
|
+
and the regex fallback finds no routine because the FUNCTION lines are
|
|
8
|
+
marked too. The fix is `dialect:` in nativegate.yaml / `--dialect` on
|
|
9
|
+
quickstart: the chosen prefix's lines become the code, the other prefix's
|
|
10
|
+
lines become comments, and the resolved copy lives under native/_expanded
|
|
11
|
+
so the untouched upstream bytes stay in native/.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import textwrap
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
from click.testing import CliRunner
|
|
18
|
+
|
|
19
|
+
from nativegate import cli
|
|
20
|
+
from nativegate.parsers.dialect import (
|
|
21
|
+
apply_dialect,
|
|
22
|
+
is_dialect_marked,
|
|
23
|
+
resolve_dialect,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# --- unit ----------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_resolve_dialect_validates():
|
|
31
|
+
assert resolve_dialect("") == ""
|
|
32
|
+
assert resolve_dialect("CS") == "cs"
|
|
33
|
+
assert resolve_dialect("cd") == "cd"
|
|
34
|
+
with pytest.raises(ValueError):
|
|
35
|
+
resolve_dialect("dc") # plausible typo must fail, not act as a no-op
|
|
36
|
+
with pytest.raises(ValueError):
|
|
37
|
+
resolve_dialect("both")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
DUAL_DIALECT = textwrap.dedent("""\
|
|
41
|
+
CS REAL FUNCTION GAMMA(X)
|
|
42
|
+
CD DOUBLE PRECISION FUNCTION DGAMMA(X)
|
|
43
|
+
CS REAL
|
|
44
|
+
CD DOUBLE PRECISION
|
|
45
|
+
1 C,CONV,EPS
|
|
46
|
+
DGAMMA = 24.0D0
|
|
47
|
+
RETURN
|
|
48
|
+
END
|
|
49
|
+
""")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_apply_dialect_selects_cd():
|
|
53
|
+
out = apply_dialect(DUAL_DIALECT, "cd")
|
|
54
|
+
lines = out.splitlines()
|
|
55
|
+
# the chosen CD lines are live, shifted two columns into statement
|
|
56
|
+
# position, with no other column moved
|
|
57
|
+
assert " DOUBLE PRECISION FUNCTION DGAMMA(X)" in lines
|
|
58
|
+
assert " DOUBLE PRECISION" in lines
|
|
59
|
+
# the rejected CS half is commented back to the normal fixed-form
|
|
60
|
+
# comment form, `C` in column 1
|
|
61
|
+
assert "C REAL FUNCTION GAMMA(X)" in lines
|
|
62
|
+
assert "C REAL" in lines
|
|
63
|
+
# untouched lines are untouched
|
|
64
|
+
assert " DGAMMA = 24.0D0" in lines
|
|
65
|
+
assert " 1 C,CONV,EPS" in lines
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_apply_dialect_selects_cs():
|
|
69
|
+
out = apply_dialect(DUAL_DIALECT, "cs")
|
|
70
|
+
lines = out.splitlines()
|
|
71
|
+
assert " REAL FUNCTION GAMMA(X)" in lines
|
|
72
|
+
assert " REAL" in lines
|
|
73
|
+
commented = [line for line in lines if line.startswith("C ")]
|
|
74
|
+
assert any("DGAMMA" in line and "DOUBLE PRECISION FUNCTION" in line
|
|
75
|
+
for line in commented)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_apply_dialect_leaves_plain_source_alone():
|
|
79
|
+
plain = " DOUBLE PRECISION FUNCTION DGAMMA(X)\n END\n"
|
|
80
|
+
assert apply_dialect(plain, "cd") == plain
|
|
81
|
+
assert apply_dialect(plain, "") == plain
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_apply_dialect_is_idempotent():
|
|
85
|
+
once = apply_dialect(DUAL_DIALECT, "cd")
|
|
86
|
+
assert apply_dialect(once, "cd") == once
|
|
87
|
+
assert not is_dialect_marked(once)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_is_dialect_marked():
|
|
91
|
+
assert is_dialect_marked(DUAL_DIALECT)
|
|
92
|
+
assert not is_dialect_marked(apply_dialect(DUAL_DIALECT, "cd"))
|
|
93
|
+
assert not is_dialect_marked("")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_apply_dialect_rejects_bad_dialect():
|
|
97
|
+
with pytest.raises(ValueError):
|
|
98
|
+
apply_dialect(DUAL_DIALECT, "dc")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --- config round-trip -----------------------------------------------------
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_config_round_trips_dialect(tmp_path):
|
|
105
|
+
"""`dialect:` survives a save/load cycle, and an unknown value is a
|
|
106
|
+
config error rather than a silently ignored key."""
|
|
107
|
+
import yaml
|
|
108
|
+
from nativegate.config import ServiceConfig
|
|
109
|
+
|
|
110
|
+
service = tmp_path / "specfun_gamma"
|
|
111
|
+
service.mkdir(parents=True)
|
|
112
|
+
(service / "nativegate.yaml").write_text(
|
|
113
|
+
yaml.dump({"name": "specfun_gamma", "language": "fortran",
|
|
114
|
+
"dialect": "cd", "expose": {"functions": []}},
|
|
115
|
+
sort_keys=False),
|
|
116
|
+
)
|
|
117
|
+
config = ServiceConfig.load(service)
|
|
118
|
+
assert config.dialect == "cd"
|
|
119
|
+
|
|
120
|
+
config.save(service)
|
|
121
|
+
reloaded = ServiceConfig.load(service)
|
|
122
|
+
assert reloaded.dialect == "cd"
|
|
123
|
+
|
|
124
|
+
(service / "nativegate.yaml").write_text(
|
|
125
|
+
yaml.dump({"name": "specfun_gamma", "language": "fortran",
|
|
126
|
+
"dialect": "dc", "expose": {}},
|
|
127
|
+
sort_keys=False),
|
|
128
|
+
)
|
|
129
|
+
with pytest.raises(Exception, match="dialect"):
|
|
130
|
+
ServiceConfig.load(service)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# --- CLI wiring -----------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.fixture()
|
|
137
|
+
def netlib_scratch(tmp_path, monkeypatch):
|
|
138
|
+
source = tmp_path / "gamma.f"
|
|
139
|
+
source.write_text(DUAL_DIALECT)
|
|
140
|
+
monkeypatch.chdir(tmp_path)
|
|
141
|
+
return source
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_quickstart_dialect_generates_and_keeps_native_untouched(netlib_scratch, tmp_path):
|
|
145
|
+
result = CliRunner().invoke(
|
|
146
|
+
cli.main,
|
|
147
|
+
["quickstart", str(netlib_scratch), "--name", "specfun_gamma",
|
|
148
|
+
"--dialect", "cd"],
|
|
149
|
+
)
|
|
150
|
+
assert result.exit_code == 0, result.output
|
|
151
|
+
|
|
152
|
+
service = tmp_path / "services" / "specfun_gamma"
|
|
153
|
+
native = service / "native" / "gamma.f"
|
|
154
|
+
assert native.read_text() == DUAL_DIALECT
|
|
155
|
+
resolved = service / "native" / "_expanded" / "gamma.f"
|
|
156
|
+
assert resolved.exists()
|
|
157
|
+
assert resolved.read_text().rstrip("\n") == apply_dialect(
|
|
158
|
+
DUAL_DIALECT, "cd"
|
|
159
|
+
).rstrip("\n")
|
|
160
|
+
|
|
161
|
+
assert "dialect: cd" in (service / "nativegate.yaml").read_text()
|
|
162
|
+
|
|
163
|
+
init = service / "python" / "specfun_gamma" / "__init__.py"
|
|
164
|
+
assert "dgamma" in init.read_text().lower()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_without_dialect_the_marked_file_fails_with_a_direction(netlib_scratch):
|
|
168
|
+
result = CliRunner().invoke(
|
|
169
|
+
cli.main, ["quickstart", str(netlib_scratch), "--name", "specfun_gamma"]
|
|
170
|
+
)
|
|
171
|
+
assert result.exit_code != 0
|
|
172
|
+
# the parser error is passed through, but the remedy is named
|
|
173
|
+
assert "dialect-marked" in result.output
|
|
174
|
+
assert "--dialect cs" in result.output and "--dialect cd" in result.output
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_bad_dialect_is_an_error_not_a_noop(netlib_scratch):
|
|
178
|
+
result = CliRunner().invoke(
|
|
179
|
+
cli.main,
|
|
180
|
+
["quickstart", str(netlib_scratch), "--name", "specfun_gamma",
|
|
181
|
+
"--dialect", "dc"],
|
|
182
|
+
)
|
|
183
|
+
assert result.exit_code != 0
|
|
184
|
+
assert "Unknown dialect" in result.output
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|