eosframes 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eosframes/__init__.py +121 -0
- eosframes/cli.py +764 -0
- eosframes/exceptions.py +20 -0
- eosframes/hub.py +152 -0
- eosframes/logger.py +127 -0
- eosframes/naming.py +610 -0
- eosframes/ops.py +711 -0
- eosframes/read.py +213 -0
- eosframes/scale.py +1713 -0
- eosframes/stack.py +204 -0
- eosframes/utils.py +23 -0
- eosframes/write.py +201 -0
- eosframes-1.1.0.dist-info/METADATA +112 -0
- eosframes-1.1.0.dist-info/RECORD +17 -0
- eosframes-1.1.0.dist-info/WHEEL +4 -0
- eosframes-1.1.0.dist-info/entry_points.txt +3 -0
- eosframes-1.1.0.dist-info/licenses/LICENSE +21 -0
eosframes/exceptions.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Custom exceptions for the ``eosframes`` library."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EosframesError(Exception):
|
|
5
|
+
"""Sole exception type raised by ``eosframes``.
|
|
6
|
+
|
|
7
|
+
Every validation failure inside the library — naming-convention
|
|
8
|
+
violations, model-ID / version mismatches, attempts to overwrite an
|
|
9
|
+
existing output, malformed scaler JSONs, HTTP failures from the model
|
|
10
|
+
hub — surfaces as ``EosframesError``. Users only need to catch this one
|
|
11
|
+
exception class to handle any library-level problem.
|
|
12
|
+
|
|
13
|
+
Examples
|
|
14
|
+
--------
|
|
15
|
+
>>> from eosframes import EosframesError, write_csv
|
|
16
|
+
>>> try:
|
|
17
|
+
... write_csv(df, "results.csv") # missing model ID in path
|
|
18
|
+
... except EosframesError as e:
|
|
19
|
+
... print(f"eosframes refused the write: {e}")
|
|
20
|
+
"""
|
eosframes/hub.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Fetch Ersilia model information from GitHub.
|
|
2
|
+
|
|
3
|
+
This module reaches out to ``raw.githubusercontent.com/ersilia-os/<model_id>``
|
|
4
|
+
to retrieve metadata (``fetch_metadata``) and per-version column
|
|
5
|
+
definitions (``fetch_columns``). ``fetch_columns`` first tries the
|
|
6
|
+
semver tag for *version* (``v<N>`` → ``v<N>.0.0``), then falls back to
|
|
7
|
+
``main``; ``fetch_metadata`` fetches from ``main`` directly.
|
|
8
|
+
|
|
9
|
+
All HTTP calls have a 15 s timeout. Failures are surfaced as
|
|
10
|
+
:class:`~eosframes.EosframesError` with a hint pointing at the
|
|
11
|
+
``ersilia-os`` repo URL.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import io
|
|
15
|
+
import json
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
import pandas as pd
|
|
19
|
+
import requests
|
|
20
|
+
|
|
21
|
+
from .exceptions import EosframesError
|
|
22
|
+
from .logger import get_logger
|
|
23
|
+
|
|
24
|
+
_GITHUB_RAW = "https://raw.githubusercontent.com/ersilia-os/{model_id}/{ref}/{filename}"
|
|
25
|
+
_METADATA_CANDIDATES = ["metadata.json", "metadata.yml", "metadata.yaml"]
|
|
26
|
+
_RUN_COLUMNS_PATH = "model/framework/columns/run_columns.csv"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _version_to_ref(version: str) -> str:
|
|
30
|
+
"""Map a short version string ``v<N>`` to its semver git tag ``v<N>.0.0``.
|
|
31
|
+
|
|
32
|
+
Anything that doesn't match the short form is returned unchanged,
|
|
33
|
+
so callers can pass already-resolved refs (full SHAs, branch
|
|
34
|
+
names) without surprise rewriting.
|
|
35
|
+
"""
|
|
36
|
+
m = re.match(r"^v(\d+)$", version)
|
|
37
|
+
if m:
|
|
38
|
+
return f"v{m.group(1)}.0.0"
|
|
39
|
+
return version
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _raw_url(model_id: str, ref: str, filename: str) -> str:
|
|
43
|
+
"""Build a ``raw.githubusercontent.com`` URL for *filename* at *ref*."""
|
|
44
|
+
return _GITHUB_RAW.format(model_id=model_id, ref=ref, filename=filename)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def fetch_metadata(model_id: str) -> dict:
|
|
48
|
+
"""Fetch metadata for an Ersilia model from GitHub.
|
|
49
|
+
|
|
50
|
+
Probes ``main`` for ``metadata.json``, ``metadata.yml`` and
|
|
51
|
+
``metadata.yaml`` in that order, returning the first one that
|
|
52
|
+
responds with HTTP 200. YAML files require ``pyyaml`` to be
|
|
53
|
+
installed.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
model_id : str
|
|
58
|
+
Ersilia model identifier.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
dict
|
|
63
|
+
Raw metadata as a dictionary.
|
|
64
|
+
|
|
65
|
+
Raises
|
|
66
|
+
------
|
|
67
|
+
EosframesError
|
|
68
|
+
If none of the candidate metadata files exist on the model's
|
|
69
|
+
``main`` branch, or if a YAML metadata file is found but
|
|
70
|
+
``pyyaml`` is not installed.
|
|
71
|
+
"""
|
|
72
|
+
logger = get_logger()
|
|
73
|
+
for filename in _METADATA_CANDIDATES:
|
|
74
|
+
url = _raw_url(model_id, "main", filename)
|
|
75
|
+
logger.debug("GET %s", url)
|
|
76
|
+
resp = requests.get(url, timeout=15)
|
|
77
|
+
if resp.status_code == 200:
|
|
78
|
+
logger.info("Fetched metadata for %s from %s", model_id, filename)
|
|
79
|
+
if filename.endswith(".json"):
|
|
80
|
+
return json.loads(resp.text)
|
|
81
|
+
try:
|
|
82
|
+
import yaml
|
|
83
|
+
|
|
84
|
+
return yaml.safe_load(resp.text)
|
|
85
|
+
except ImportError as exc:
|
|
86
|
+
raise EosframesError(
|
|
87
|
+
f"Model '{model_id}' has a YAML metadata file but 'pyyaml' is not "
|
|
88
|
+
"installed. Install it with: pip install pyyaml"
|
|
89
|
+
) from exc
|
|
90
|
+
logger.debug(
|
|
91
|
+
"Metadata candidate %s returned HTTP %d for %s",
|
|
92
|
+
filename,
|
|
93
|
+
resp.status_code,
|
|
94
|
+
model_id,
|
|
95
|
+
)
|
|
96
|
+
raise EosframesError(
|
|
97
|
+
f"Could not fetch metadata for model '{model_id}'. "
|
|
98
|
+
f"Make sure the repo exists at https://github.com/ersilia-os/{model_id}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def fetch_columns(model_id: str, version: str) -> pd.DataFrame:
|
|
103
|
+
"""Fetch the ``run_columns.csv`` for an Ersilia model version from GitHub.
|
|
104
|
+
|
|
105
|
+
Tries the semver tag for *version* first (``v<N>`` → ``v<N>.0.0``),
|
|
106
|
+
then falls back to ``main``. The first ref returning HTTP 200 wins.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
model_id : str
|
|
111
|
+
Ersilia model identifier.
|
|
112
|
+
version : str
|
|
113
|
+
Version string matching ``v\\d+``. Resolved by :func:`_version_to_ref`.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
pandas.DataFrame
|
|
118
|
+
Parsed ``run_columns.csv``, typically with columns ``name``,
|
|
119
|
+
``type``, ``direction``, ``description``.
|
|
120
|
+
|
|
121
|
+
Raises
|
|
122
|
+
------
|
|
123
|
+
EosframesError
|
|
124
|
+
If neither the tagged ref nor ``main`` returns the file.
|
|
125
|
+
"""
|
|
126
|
+
logger = get_logger()
|
|
127
|
+
refs_to_try = list(
|
|
128
|
+
dict.fromkeys([_version_to_ref(version), "main"])
|
|
129
|
+
) # ordered, unique
|
|
130
|
+
for ref in refs_to_try:
|
|
131
|
+
url = _raw_url(model_id, ref, _RUN_COLUMNS_PATH)
|
|
132
|
+
logger.debug("GET %s", url)
|
|
133
|
+
resp = requests.get(url, timeout=15)
|
|
134
|
+
if resp.status_code == 200:
|
|
135
|
+
logger.info(
|
|
136
|
+
"Fetched run_columns.csv for %s @ %s (resolved from version=%s)",
|
|
137
|
+
model_id,
|
|
138
|
+
ref,
|
|
139
|
+
version,
|
|
140
|
+
)
|
|
141
|
+
return pd.read_csv(io.StringIO(resp.text))
|
|
142
|
+
logger.debug(
|
|
143
|
+
"run_columns.csv at %s returned HTTP %d for %s",
|
|
144
|
+
ref,
|
|
145
|
+
resp.status_code,
|
|
146
|
+
model_id,
|
|
147
|
+
)
|
|
148
|
+
raise EosframesError(
|
|
149
|
+
f"Could not fetch run_columns.csv for model '{model_id}' at any of "
|
|
150
|
+
f"the candidate refs {refs_to_try}. "
|
|
151
|
+
f"Make sure the repo exists at https://github.com/ersilia-os/{model_id}"
|
|
152
|
+
)
|
eosframes/logger.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Singleton logger for the ``eosframes`` library.
|
|
2
|
+
|
|
3
|
+
The logger is named ``eosframes`` and configured once on first access. It
|
|
4
|
+
prefers ``rich.logging.RichHandler`` for human-friendly CLI output, falling
|
|
5
|
+
back to a plain stderr ``StreamHandler`` with a compact ``HH:MM:SS LEVEL
|
|
6
|
+
message`` format. Output goes to ``stderr`` so it stays out of piped stdout.
|
|
7
|
+
|
|
8
|
+
The default level is ``INFO`` — operational progress messages from
|
|
9
|
+
:mod:`eosframes.ops`, :mod:`eosframes.read`, and :mod:`eosframes.write` are
|
|
10
|
+
visible immediately. Two ways to change it:
|
|
11
|
+
|
|
12
|
+
* **Environment** — ``EOSFRAMES_LOG_LEVEL=DEBUG`` (or ``WARNING`` /
|
|
13
|
+
``ERROR`` / ``CRITICAL``) before the process starts.
|
|
14
|
+
* **Programmatic** — :func:`set_verbosity` toggles between ``DEBUG`` and
|
|
15
|
+
``INFO``. Useful in notebooks and library callers.
|
|
16
|
+
|
|
17
|
+
Level colour styling matches ``ersilia-os/lazy-qsar`` (debug=cyan,
|
|
18
|
+
info=blue, warning=yellow, error=red, critical=white-on-red).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
_logger = None # singleton
|
|
26
|
+
|
|
27
|
+
_LEVEL_THEME = {
|
|
28
|
+
"logging.level.debug": "bold cyan",
|
|
29
|
+
"logging.level.info": "bold blue",
|
|
30
|
+
"logging.level.warning": "bold yellow",
|
|
31
|
+
"logging.level.error": "bold red",
|
|
32
|
+
"logging.level.critical": "bold white on red",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_logger() -> logging.Logger:
|
|
37
|
+
"""Return the singleton ``eosframes`` logger.
|
|
38
|
+
|
|
39
|
+
The logger is configured on first call and reused thereafter. Subsequent
|
|
40
|
+
calls return the same instance without re-installing handlers.
|
|
41
|
+
|
|
42
|
+
Behaviour
|
|
43
|
+
---------
|
|
44
|
+
* Uses ``rich.logging.RichHandler`` (on a stderr ``Console``) when
|
|
45
|
+
``rich`` is importable, falling back to ``logging.StreamHandler`` on
|
|
46
|
+
stderr.
|
|
47
|
+
* Default level is ``INFO``. Override with the ``EOSFRAMES_LOG_LEVEL``
|
|
48
|
+
environment variable (case-insensitive: ``DEBUG`` / ``INFO`` /
|
|
49
|
+
``WARNING`` / ``ERROR`` / ``CRITICAL``) or with :func:`set_verbosity`
|
|
50
|
+
at runtime. Invalid env values fall back to ``INFO``.
|
|
51
|
+
* ``propagate = False`` so library records don't bubble up to the root
|
|
52
|
+
logger and produce duplicate output in host applications.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
logging.Logger
|
|
57
|
+
The shared ``eosframes`` logger instance.
|
|
58
|
+
"""
|
|
59
|
+
global _logger
|
|
60
|
+
if _logger is not None:
|
|
61
|
+
return _logger
|
|
62
|
+
|
|
63
|
+
logger = logging.getLogger("eosframes")
|
|
64
|
+
logger.setLevel(_resolve_level())
|
|
65
|
+
# Don't double-log via the root logger when callers configure their own
|
|
66
|
+
# handlers (e.g. a Flask/Django app importing eosframes).
|
|
67
|
+
logger.propagate = False
|
|
68
|
+
|
|
69
|
+
if not logger.handlers:
|
|
70
|
+
logger.addHandler(_build_handler())
|
|
71
|
+
|
|
72
|
+
_logger = logger
|
|
73
|
+
return _logger
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def set_verbosity(verbose: bool) -> None:
|
|
77
|
+
"""Toggle verbose (``DEBUG``) logging on or off.
|
|
78
|
+
|
|
79
|
+
``verbose=True`` lowers the level to ``DEBUG`` (everything visible);
|
|
80
|
+
``verbose=False`` restores it to ``INFO`` — the default operational
|
|
81
|
+
level, where progress messages from readers, writers, and ops are still
|
|
82
|
+
shown but per-HTTP-probe / per-chunk traces are hidden. Warnings and
|
|
83
|
+
errors are emitted at both settings.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
verbose : bool
|
|
88
|
+
"""
|
|
89
|
+
logger = get_logger()
|
|
90
|
+
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _build_handler() -> logging.Handler:
|
|
94
|
+
"""Build the singleton stderr handler, preferring rich's ``RichHandler``."""
|
|
95
|
+
try:
|
|
96
|
+
from rich.console import Console
|
|
97
|
+
from rich.logging import RichHandler
|
|
98
|
+
from rich.theme import Theme
|
|
99
|
+
|
|
100
|
+
console = Console(stderr=True, theme=Theme(_LEVEL_THEME))
|
|
101
|
+
handler: logging.Handler = RichHandler(
|
|
102
|
+
console=console,
|
|
103
|
+
rich_tracebacks=True,
|
|
104
|
+
show_path=False,
|
|
105
|
+
show_time=True,
|
|
106
|
+
markup=False,
|
|
107
|
+
log_time_format="%H:%M:%S",
|
|
108
|
+
)
|
|
109
|
+
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
110
|
+
return handler
|
|
111
|
+
except ImportError:
|
|
112
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
113
|
+
handler.setFormatter(
|
|
114
|
+
logging.Formatter(
|
|
115
|
+
"%(asctime)s %(levelname)-8s %(message)s", datefmt="%H:%M:%S"
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
return handler
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _resolve_level() -> int:
|
|
122
|
+
"""Resolve the logging level from ``EOSFRAMES_LOG_LEVEL``, defaulting to INFO."""
|
|
123
|
+
raw = os.environ.get("EOSFRAMES_LOG_LEVEL", "").strip().upper()
|
|
124
|
+
if not raw:
|
|
125
|
+
return logging.INFO
|
|
126
|
+
level = logging.getLevelName(raw)
|
|
127
|
+
return level if isinstance(level, int) else logging.INFO
|