Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pip.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
fail-fast: false
matrix:
platform: [windows-latest, macos-latest, ubuntu-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v6
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies = [
'gitpython>=3.1.43',
'msgspec>=0.19.0',
'packaging',
'pyyaml',
]
description = "ANTLR4 grammars for McStas and McXtrace"
readme = "README.md"
Expand All @@ -30,11 +31,11 @@ classifiers = [
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
dynamic = ["version"]

Expand Down
106 changes: 94 additions & 12 deletions src/mccode_antlr/cli/management.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,103 @@ def config_get(key, verbose):
print(config_dump(d))


def config_set(key, value):
from mccode_antlr.config import config as c
levels = key.split('.')
for level in levels:
c = c[level]
c.set(value)
def _get_config_yaml(path: str | None = None):
from pathlib import Path
from yaml import safe_load
from mccode_antlr.config import config

config_file = Path(path or config.config_dir()) / 'config.yaml'
if config_file.exists():
with config_file.open('r') as f:
return safe_load(f) or {}
return {}

def config_unset(key):
from mccode_antlr.config import config as c

def _save_config_yaml(config_dict, path: str | None = None):
from pathlib import Path
from mccode_antlr.config import config

config_file = Path(path or config.config_dir()) / 'config.yaml'
config_file.parent.mkdir(parents=True, exist_ok=True)
with config_file.open('w') as f:
f.write(config_dump(config_dict))


def config_set(key, value, path: str | None = None):
"""
Set a configuration value both in the file and in memory.
This function updates the existing configuration file to preserve
other keys and only modify the specified key.
"""
from mccode_antlr.config import config
from pathlib import Path
# Load existing file config if it exists
existing_config = _get_config_yaml(path)

# Navigate to the nested key and set the value
levels = key.split('.')
try:
current = existing_config
for level in levels[:-1]:
if level not in current:
current[level] = {}
current = current[level]
current[levels[-1]] = value
except (KeyError, TypeError):
print(f"Error setting key {key} in configuration.")
return

# Save the complete config
_save_config_yaml(existing_config, path)

# Also update the in-memory config if we're not modifying a different path
if path is None or Path(path) == Path(config.config_dir()):
try:
c = config
for level in levels[:-1]:
c = c[level]
c[levels[-1]] = value
except (KeyError, TypeError):
print(f"Error setting key {key} in in-memory configuration.")
return


def config_unset(key, path: str | None = None):
"""
Unset a configuration value both in the file and in memory.
This function updates the existing configuration file to preserve
other keys and only remove the specified key.
"""
from mccode_antlr.config import config
from pathlib import Path
existing_config = _get_config_yaml(path)

# Navigate to the nested key and delete it
levels = key.split('.')
for level in levels:
c = c[level]
del c
current = existing_config
try:
for level in levels[:-1]:
current = current[level]
del current[levels[-1]]
except (KeyError, TypeError):
# Key doesn't exist in file config, that's okay
pass

# Save the complete config
_save_config_yaml(existing_config, path)

# Also update the in-memory config if we're not modifying a different path
if path is None or Path(path) == Path(config.config_dir()):
try:
c = config
for level in levels[:-1]:
c = c[level]
del c[levels[-1]]
except (KeyError, TypeError):
pass


def config_save(path, verbose):
def config_save(path: str | None = None, verbose: bool = False):
from pathlib import Path
from mccode_antlr.config import config as c
config_dir = Path(path or c.config_dir())
Expand All @@ -79,10 +159,12 @@ def add_config_management_parser(modes):
s = actions.add_parser(name='set', help='Update or insert one configuration value')
s.add_argument('key', type=str, default=None)
s.add_argument('value', type=str, default=None)
s.add_argument('path', type=str, nargs='?')
s.set_defaults(action=config_set)

u = actions.add_parser(name='unset', help='Remove one configuration value')
u.add_argument('key', type=str, default=None)
u.add_argument('path', type=str, nargs='?')
u.set_defaults(action=config_unset)

v = actions.add_parser(name='save', help='Create or update the configuration file')
Expand Down
37 changes: 35 additions & 2 deletions src/mccode_antlr/reader/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,40 @@
from mccode_antlr import Flavor
from typing import Type, Any
from msgspec import Struct
import requests
from time import sleep


def _fetch_registry_with_retry(url, max_retries=3, timeout=10):
"""Fetch a registry file with retry logic for gateway timeouts"""
for attempt in range(max_retries):
try:
r = requests.get(url, timeout=timeout)
if r.ok:
return r
# If we get a gateway timeout (502, 503, 504), retry
if r.status_code in (502, 503, 504) and attempt < max_retries - 1:
wait_time = 2 ** attempt # exponential backoff: 1s, 2s, 4s
logger.warning(f"Gateway timeout ({r.status_code}), retrying in {wait_time}s...")
sleep(wait_time)
continue
return r
except requests.exceptions.Timeout:
if attempt < max_retries - 1:
wait_time = 2 ** attempt
logger.warning(f"Request timeout, retrying in {wait_time}s...")
sleep(wait_time)
continue
raise
except requests.exceptions.RequestException as e:
if attempt < max_retries - 1:
wait_time = 2 ** attempt
logger.warning(f"Request failed: {e}, retrying in {wait_time}s...")
sleep(wait_time)
continue
raise

raise RuntimeError(f"Failed to retrieve {url} after {max_retries} attempts")


def ensure_regex_pattern(pattern):
Expand Down Expand Up @@ -250,11 +284,10 @@ def __init__(self, name: str, url: str, version: str, filename: str | None = Non
with registry_file_path.open('r') as file:
registry = {k: v for k, v in [x.strip().split(maxsplit=1) for x in file.readlines() if len(x)]}
else:
import requests
# We allow a full-dictionary to be provided, otherwise we expect the registry file to be available from the
# base_url where all subsequent files are also expected to be available
if not isinstance(registry, dict):
r = requests.get((registry or base_url) + registry_file)
r = _fetch_registry_with_retry((registry or base_url) + registry_file)
if not r.ok:
raise RuntimeError(f"Could not retrieve {r.url} because {r.reason}")
registry = {k: v for k, v in [x.split(maxsplit=1) for x in r.text.split('\n') if len(x)]}
Expand Down
Loading