Skip to content

Commit 0c2d4be

Browse files
authored
Merge pull request #70 from OpenCOMPES/elab_metadata_integration
Elab metadata integration
2 parents 6eca8e4 + 6fedc6b commit 0c2d4be

File tree

20 files changed

+1565
-432
lines changed

20 files changed

+1565
-432
lines changed

.cspell/custom-dictionary.txt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
allclose
33
ALLUSERSPROFILE
44
amperemeter
5+
appauthor
6+
appname
57
arange
68
archiver
79
argwhere
@@ -18,6 +20,7 @@ basepath
1820
bitshift
1921
bysource
2022
calib
23+
caplog
2124
checkscan
2225
clim
2326
codemirror
@@ -32,16 +35,21 @@ dapolymatrix
3235
dataconverter
3336
dataframe
3437
delaystage
38+
delenv
3539
dtype
3640
dxda
3741
dxde
3842
dyda
3943
dyde
4044
Ekin
41-
electronanalyser
45+
elab
46+
elabapi
47+
elabid
48+
electronanalyzer
4249
elems
4350
endstation
4451
energydispersion
52+
entityid
4553
eshift
4654
faddr
4755
Faradayweg
@@ -72,6 +80,7 @@ kwds
7280
labview
7381
Laurenz
7482
lensmodes
83+
levelname
7584
lineh
7685
linev
7786
listf
@@ -102,6 +111,7 @@ Nxpix
102111
Nxpixels
103112
Nypixels
104113
OPCPA
114+
orcid
105115
pcolormesh
106116
Phoibos
107117
polyfit
@@ -120,6 +130,7 @@ rrvec
120130
rtol
121131
rtype
122132
scanvector
133+
sharelink
123134
specsanalyzer
124135
Specslab
125136
specsscan
@@ -131,9 +142,11 @@ toctree
131142
tomlkit
132143
topfloor
133144
tqdm
145+
trarpes
134146
typehints
135147
TZCYXS
136148
undoc
149+
userid
137150
venv
138151
viewcode
139152
vline

docs/specsanalyzer/config.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ The config module contains a mechanics to collect configuration parameters from
44
It will load an (optional) provided config file, or alternatively use a passed python dictionary as initial config dictionary, and subsequently look for the following additional config files to load:
55

66
* ``folder_config``: A config file of name :file:`specs_config.yaml` in the current working directory. This is mostly intended to pass calibration parameters of the workflow between different notebook instances.
7-
* ``user_config``: A config file provided by the user, stored as :file:`.specsanalyzer/config.yaml` in the current user's home directly. This is intended to give a user the option for individual configuration modifications of system settings.
8-
* ``system_config``: A config file provided by the system administrator, stored as :file:`/etc/specsanalyzer/config.yaml` on Linux-based systems, and :file:`%ALLUSERSPROFILE%/specsanalyzer/config.yaml` on Windows. This should provide all necessary default parameters for using the specsanalyzer processor with a given setup. For an example for the setup at the Fritz Haber Institute setup, see :ref:`example_config`
7+
* ``user_config``: A config file provided by the user, stored as :file:`.config/specsanalyzer/config_v1.yaml` in the current user's home directly. This is intended to give a user the option for individual configuration modifications of system settings.
8+
* ``system_config``: A config file provided by the system administrator, stored as :file:`/etc/specsanalyzer/config_v1.yaml` on Linux-based systems, and :file:`%ALLUSERSPROFILE%/specsanalyzer/config_v1.yaml` on Windows. This should provide all necessary default parameters for using the specsanalyzer processor with a given setup. For an example for the setup at the Fritz Haber Institute setup, see :ref:`example_config`
99
* ``default_config``: The default configuration shipped with the package. Typically, all parameters here should be overwritten by any of the other configuration files.
1010

1111
The config mechanism returns the combined dictionary, and reports the loaded configuration files. In order to disable or overwrite any of the configuration files, they can be also given as optional parameters (path to a file, or python dictionary).

docs/specsscan/helpers.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ Helpers
33
.. automodule:: specsscan.helpers
44
:members:
55
:undoc-members:
6+
7+
.. automodule:: specsscan.metadata
8+
:members:
9+
:undoc-members:

pyproject.toml

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@ classifiers = [
3232
"Operating System :: OS Independent",
3333
]
3434
dependencies = [
35+
"elabapi-python>=5.0",
3536
"h5py>=3.6.0",
3637
"imutils>=0.5.4",
3738
"ipympl>=0.9.1",
3839
"ipywidgets>=7.7.1",
39-
"matplotlib>=3.5.1,<3.10.0",
40+
"matplotlib>=3.5.1",
4041
"numpy>=1.21.6",
4142
"opencv-python>=4.8.1.78",
42-
"pynxtools-mpes>=0.2.1",
43-
"pynxtools>=0.9.3",
43+
"pynxtools-mpes>=0.2.2",
44+
"pynxtools>=0.10.1",
4445
"python-dateutil>=2.8.2",
4546
"pyyaml>=6.0",
4647
"xarray>=0.20.2",
@@ -83,12 +84,6 @@ all = [
8384
"specsanalyzer[dev,docs,notebook]",
8485
]
8586

86-
[tool.coverage.report]
87-
omit = [
88-
"config.py",
89-
"config-3.py",
90-
]
91-
9287
[tool.ruff]
9388
include = ["specsanalyzer/*.py", "specsscan/*.py", "tests/*.py"]
9489
lint.select = [

src/specsanalyzer/config.py

Lines changed: 117 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,35 @@
11
"""This module contains a config library for loading yaml/json files into dicts"""
22
from __future__ import annotations
33

4+
import copy
45
import json
56
import os
67
import platform
78
from importlib.util import find_spec
89
from pathlib import Path
910

1011
import yaml
12+
from platformdirs import user_config_path
13+
14+
from specsanalyzer.logging import setup_logging
1115

1216
package_dir = os.path.dirname(find_spec("specsanalyzer").origin)
1317

18+
USER_CONFIG_PATH = user_config_path(
19+
appname="specsanalyzer",
20+
appauthor="OpenCOMPES",
21+
ensure_exists=True,
22+
)
23+
SYSTEM_CONFIG_PATH = (
24+
Path(os.environ["ALLUSERSPROFILE"]).joinpath("specsanalyzer")
25+
if platform.system() == "Windows"
26+
else Path("/etc/").joinpath("specsanalyzer")
27+
)
28+
ENV_DIR = Path(".env")
29+
30+
# Configure logging
31+
logger = setup_logging("config")
32+
1433

1534
def parse_config(
1635
config: dict | str = None,
@@ -36,12 +55,13 @@ def parse_config(
3655
user_config (dict | str, optional): user-based config dictionary
3756
or file path. The loaded dictionary is completed with the user-based values,
3857
taking preference over system and default values.
39-
Defaults to the file ".specsanalyzer/config.yaml" in the current user's home directory.
58+
Defaults to the file ".config/specsanalyzer/config_v1.yaml" in the current user's home
59+
directory.
4060
system_config (dict | str, optional): system-wide config dictionary
4161
or file path. The loaded dictionary is completed with the system-wide values,
4262
taking preference over default values.
43-
Defaults to the file "/etc/specsanalyzer/config.yaml" on linux,
44-
and "%ALLUSERSPROFILE%/specsanalyzer/config.yaml" on windows.
63+
Defaults to the file "/etc/specsanalyzer/config_v1.yaml" on linux,
64+
and "%ALLUSERSPROFILE%/specsanalyzer/config_v1.yaml" on windows.
4565
default_config (dict | str, optional): default config dictionary
4666
or file path. The loaded dictionary is completed with the default values.
4767
Defaults to *package_dir*/config/default.yaml".
@@ -57,62 +77,51 @@ def parse_config(
5777
config = {}
5878

5979
if isinstance(config, dict):
60-
config_dict = config
80+
config_dict = copy.deepcopy(config)
6181
else:
6282
config_dict = load_config(config)
6383
if verbose:
64-
print(f"Configuration loaded from: [{str(Path(config).resolve())}]")
84+
logger.info(f"Configuration loaded from: [{str(Path(config).resolve())}]")
6585

6686
folder_dict: dict = None
6787
if isinstance(folder_config, dict):
68-
folder_dict = folder_config
88+
folder_dict = copy.deepcopy(folder_config)
6989
else:
7090
if folder_config is None:
7191
folder_config = "./specs_config.yaml"
7292
if Path(folder_config).exists():
7393
folder_dict = load_config(folder_config)
7494
if verbose:
75-
print(f"Folder config loaded from: [{str(Path(folder_config).resolve())}]")
95+
logger.info(f"Folder config loaded from: [{str(Path(folder_config).resolve())}]")
7696

7797
user_dict: dict = None
7898
if isinstance(user_config, dict):
79-
user_dict = user_config
99+
user_dict = copy.deepcopy(user_config)
80100
else:
81101
if user_config is None:
82-
user_config = str(
83-
Path.home().joinpath(".specsanalyzer").joinpath("config.yaml"),
84-
)
102+
user_config = str(USER_CONFIG_PATH.joinpath("config_v1.yaml"))
85103
if Path(user_config).exists():
86104
user_dict = load_config(user_config)
87105
if verbose:
88-
print(f"User config loaded from: [{str(Path(user_config).resolve())}]")
106+
logger.info(f"User config loaded from: [{str(Path(user_config).resolve())}]")
89107

90108
system_dict: dict = None
91109
if isinstance(system_config, dict):
92-
system_dict = system_config
110+
system_dict = copy.deepcopy(system_config)
93111
else:
94112
if system_config is None:
95-
if platform.system() in ["Linux", "Darwin"]:
96-
system_config = str(
97-
Path("/etc/").joinpath("specsanalyzer").joinpath("config.yaml"),
98-
)
99-
elif platform.system() == "Windows":
100-
system_config = str(
101-
Path(os.environ["ALLUSERSPROFILE"])
102-
.joinpath("specsanalyzer")
103-
.joinpath("config.yaml"),
104-
)
113+
system_config = str(SYSTEM_CONFIG_PATH.joinpath("config_v1.yaml"))
105114
if Path(system_config).exists():
106115
system_dict = load_config(system_config)
107116
if verbose:
108-
print(f"System config loaded from: [{str(Path(system_config).resolve())}]")
117+
logger.info(f"System config loaded from: [{str(Path(system_config).resolve())}]")
109118

110119
if isinstance(default_config, dict):
111-
default_dict = default_config
120+
default_dict = copy.deepcopy(default_config)
112121
else:
113122
default_dict = load_config(default_config)
114123
if verbose:
115-
print(f"Default config loaded from: [{str(Path(default_config).resolve())}]")
124+
logger.info(f"Default config loaded from: [{str(Path(default_config).resolve())}]")
116125

117126
if folder_dict is not None:
118127
config_dict = complete_dictionary(
@@ -226,3 +235,85 @@ def complete_dictionary(dictionary: dict, base_dictionary: dict) -> dict:
226235
dictionary[k] = v
227236

228237
return dictionary
238+
239+
240+
def _parse_env_file(file_path: Path) -> dict:
241+
"""Helper function to parse a .env file into a dictionary.
242+
243+
Args:
244+
file_path (Path): Path to the .env file
245+
246+
Returns:
247+
dict: Dictionary of environment variables from the file
248+
"""
249+
env_content = {}
250+
if file_path.exists():
251+
with open(file_path) as f:
252+
for line in f:
253+
line = line.strip()
254+
if line and "=" in line:
255+
key, val = line.split("=", 1)
256+
env_content[key.strip()] = val.strip()
257+
return env_content
258+
259+
260+
def read_env_var(var_name: str) -> str | None:
261+
"""Read an environment variable from multiple locations in order:
262+
1. OS environment variables
263+
2. .env file in current directory
264+
3. .env file in user config directory
265+
4. .env file in system config directory
266+
267+
Args:
268+
var_name (str): Name of the environment variable to read
269+
270+
Returns:
271+
str | None: Value of the environment variable or None if not found
272+
"""
273+
# 1. check OS environment variables
274+
value = os.getenv(var_name)
275+
if value is not None:
276+
logger.debug(f"Found {var_name} in OS environment variables")
277+
return value
278+
279+
# 2. check .env in current directory
280+
local_vars = _parse_env_file(ENV_DIR)
281+
if var_name in local_vars:
282+
logger.debug(f"Found {var_name} in ./.env file")
283+
return local_vars[var_name]
284+
285+
# 3. check .env in user config directory
286+
user_vars = _parse_env_file(USER_CONFIG_PATH / ".env")
287+
if var_name in user_vars:
288+
logger.debug(f"Found {var_name} in user config .env file")
289+
return user_vars[var_name]
290+
291+
# 4. check .env in system config directory
292+
system_vars = _parse_env_file(SYSTEM_CONFIG_PATH / ".env")
293+
if var_name in system_vars:
294+
logger.debug(f"Found {var_name} in system config .env file")
295+
return system_vars[var_name]
296+
297+
logger.debug(f"Environment variable {var_name} not found in any location")
298+
return None
299+
300+
301+
def save_env_var(var_name: str, value: str) -> None:
302+
"""Save an environment variable to the .env file in the user config directory.
303+
If the file exists, preserves other variables. If not, creates a new file.
304+
305+
Args:
306+
var_name (str): Name of the environment variable to save
307+
value (str): Value to save for the environment variable
308+
"""
309+
env_path = USER_CONFIG_PATH / ".env"
310+
env_content = _parse_env_file(env_path)
311+
312+
# Update or add new variable
313+
env_content[var_name] = value
314+
315+
# Write all variables back to file
316+
with open(env_path, "w") as f:
317+
for key, val in env_content.items():
318+
f.write(f"{key}={val}\n")
319+
logger.debug(f"Environment variable {var_name} saved to .env file")

src/specsanalyzer/convert.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
"""Specsanalyzer image conversion module"""
22
from __future__ import annotations
33

4+
import logging
5+
46
import numpy as np
57
from scipy.ndimage import map_coordinates
68

9+
# Configure logging
10+
logger = logging.getLogger("specsanalyzer.specsscan")
11+
712

813
def get_damatrix_from_calib2d(
914
lens_mode: str,
@@ -82,7 +87,7 @@ def get_damatrix_from_calib2d(
8287

8388
elif lens_mode in supported_space_modes:
8489
# use the mode defaults
85-
print("This is a spatial mode, using default " + lens_mode + " config")
90+
logger.info("This is a spatial mode, using default " + lens_mode + " config")
8691
rr_vec, da_matrix_full = get_rr_da(lens_mode, calib2d_dict)
8792
a_inner = da_matrix_full[0][0]
8893
da_matrix = da_matrix_full[1:][:]

0 commit comments

Comments
 (0)