diracx-testing 0.0.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.
@@ -0,0 +1,98 @@
1
+ # Python
2
+ *.py[cod]
3
+
4
+ # Conda
5
+ .conda
6
+
7
+ # C extensions
8
+ *.so
9
+ var
10
+ sdist
11
+ lib
12
+ lib64
13
+
14
+ # Packages
15
+ *.egg
16
+ *.egg-info
17
+ dist
18
+ build
19
+ eggs
20
+ parts
21
+ bin
22
+ develop-eggs
23
+ .installed.cfg
24
+ *.whl
25
+
26
+ # Translations
27
+ *.mo
28
+
29
+ # Mr Developer
30
+ .mr.developer.cfg
31
+
32
+ # Installer logs
33
+ pip-log.txt
34
+
35
+ # Unit test / coverage reports
36
+ .coverage
37
+ .tox
38
+ .ruff_cache
39
+ .mypy_cache
40
+
41
+ # Eclipse
42
+ .project
43
+ .pydevproject
44
+ .pyproject
45
+ .settings
46
+ .metadata
47
+
48
+ #VSCode
49
+ .vscode
50
+ .env
51
+
52
+ # Vim
53
+ .*.sw[a-z]
54
+ *.un~
55
+ Session.vim
56
+ *~
57
+
58
+ # Intellij
59
+ .idea/
60
+ LHCbDIRAC.iml
61
+
62
+ # MaxOSX files
63
+ .DS_Store
64
+
65
+ # test stuff
66
+ .pytest_cache
67
+ .cache
68
+ __pycache__
69
+ pytests.xml
70
+ nosetests.xml
71
+ coverage.xml
72
+ Local_*
73
+ .hypothesis
74
+ *.gz
75
+ htmlcov/
76
+ *.xml.temp
77
+
78
+ #remove in case we want a specific LHCb one
79
+ .pylintrc
80
+
81
+ # docs
82
+ # this is auto generated
83
+ docs/source/CodeDocumentation/
84
+ docs/source/AdministratorGuide/Configuration/ExampleConfig.rst
85
+ docs/source/AdministratorGuide/CommandReference
86
+ docs/source/UserGuide/CommandReference
87
+ docs/_build
88
+ docs/source/_build
89
+
90
+
91
+ # CMT junk
92
+ /*/*/x86_64-*-*-*/
93
+ */*/cmt/Makefile
94
+
95
+ # pixi environments
96
+ .pixi
97
+ pixi.lock
98
+ *.egg-info
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: diracx-testing
3
+ Version: 0.0.4
4
+ Summary: TODO
5
+ License: GPL-3.0-only
6
+ Classifier: Intended Audience :: Science/Research
7
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Topic :: Scientific/Engineering
10
+ Classifier: Topic :: System :: Distributed Computing
11
+ Requires-Python: >=3.11
12
+ Requires-Dist: httpx
13
+ Requires-Dist: joserfc
14
+ Requires-Dist: pytest
15
+ Requires-Dist: pytest-asyncio==1.0.0
16
+ Requires-Dist: pytest-cov
17
+ Requires-Dist: pytest-github-actions-annotate-failures
18
+ Requires-Dist: pytest-xdist
19
+ Requires-Dist: uuid-utils
20
+ Provides-Extra: testing
File without changes
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "diracx-testing"
3
+ description = "TODO"
4
+ readme = "README.md"
5
+ requires-python = ">=3.11"
6
+ keywords = []
7
+ license = {text = "GPL-3.0-only"}
8
+ classifiers = [
9
+ "Intended Audience :: Science/Research",
10
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
11
+ "Programming Language :: Python :: 3",
12
+ "Topic :: Scientific/Engineering",
13
+ "Topic :: System :: Distributed Computing",
14
+ ]
15
+ dependencies = [
16
+ "pytest",
17
+ "pytest-asyncio==1.0.0",
18
+ "pytest-cov",
19
+ "pytest-xdist",
20
+ "httpx",
21
+ "joserfc",
22
+ "uuid-utils",
23
+ "pytest-github-actions-annotate-failures",
24
+ ]
25
+ dynamic = ["version"]
26
+
27
+ [project.optional-dependencies]
28
+ testing = [
29
+ "diracx-testing",
30
+ ]
31
+
32
+ [build-system]
33
+ requires = ["hatchling", "hatch-vcs"]
34
+ build-backend = "hatchling.build"
35
+
36
+ [tool.hatch.version]
37
+ source = "vcs"
38
+
39
+ [tool.hatch.version.raw-options]
40
+ root = ".."
41
+
42
+ [tool.hatch.build.targets.wheel]
43
+ packages = ["src/diracx"]
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from .entrypoints import verify_entry_points
4
+ from .utils import (
5
+ ClientFactory,
6
+ aio_moto,
7
+ cli_env,
8
+ client_factory,
9
+ demo_dir,
10
+ demo_kubectl_env,
11
+ demo_urls,
12
+ do_device_flow_with_dex,
13
+ fernet_key,
14
+ private_key,
15
+ pytest_addoption,
16
+ session_client_factory,
17
+ test_auth_settings,
18
+ test_dev_settings,
19
+ test_login,
20
+ test_sandbox_settings,
21
+ with_cli_login,
22
+ with_config_repo,
23
+ )
24
+
25
+ __all__ = (
26
+ "verify_entry_points",
27
+ "ClientFactory",
28
+ "do_device_flow_with_dex",
29
+ "test_login",
30
+ "pytest_addoption",
31
+ "private_key",
32
+ "fernet_key",
33
+ "test_dev_settings",
34
+ "test_auth_settings",
35
+ "aio_moto",
36
+ "test_sandbox_settings",
37
+ "session_client_factory",
38
+ "client_factory",
39
+ "with_config_repo",
40
+ "demo_dir",
41
+ "demo_urls",
42
+ "demo_kubectl_env",
43
+ "cli_env",
44
+ "with_cli_login",
45
+ )
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import shlex
5
+ import subprocess
6
+ from importlib.resources import as_file, files
7
+ from pathlib import Path
8
+ from typing import NoReturn
9
+
10
+ SCRIPTS_BASE = files("diracx.testing").joinpath("scripts")
11
+
12
+
13
+ def parse_args() -> None:
14
+ """Access to various utility scripts for testing DiracX and extensions."""
15
+ parser = argparse.ArgumentParser(description="Utility for testing DiracX.")
16
+ sp = parser.add_subparsers(dest="command", required=True)
17
+
18
+ # Create the 'coverage' argument group.
19
+ coverage_p = sp.add_parser("coverage", help="Coverage related commands")
20
+ coverage_sp = coverage_p.add_subparsers(dest="subcommand", required=True)
21
+
22
+ # Add the 'collect-demo' command under 'coverage'.
23
+ collect_demo_p = coverage_sp.add_parser(
24
+ "collect-demo", help="Collect demo coverage"
25
+ )
26
+ collect_demo_p.add_argument(
27
+ "--demo-dir",
28
+ required=True,
29
+ type=Path,
30
+ help="Path to the .demo dir of the diracx-charts repo.",
31
+ )
32
+ collect_demo_p.set_defaults(func=lambda a: coverage_collect_demo(a.demo_dir))
33
+
34
+ args = parser.parse_args()
35
+ args.func(args)
36
+
37
+
38
+ def coverage_collect_demo(demo_dir: Path) -> NoReturn:
39
+ """Collect coverage data from a running instance of the demo.
40
+
41
+ This script is primarily intended for use in CI/CD pipelines.
42
+ """
43
+ from diracx.core.extensions import extensions_by_priority
44
+
45
+ client_extension_name = min(extensions_by_priority(), key=lambda x: x == "diracx")
46
+
47
+ with as_file(SCRIPTS_BASE / "collect_demo_coverage.sh") as script_file:
48
+ cmd = ["bash", str(script_file), "--demo-dir", str(demo_dir)]
49
+ if client_extension_name in {"diracx", "gubbins"}:
50
+ cmd += ["--diracx-repo", str(Path.cwd())]
51
+ if client_extension_name == "gubbins":
52
+ cmd += ["--extension-name", "gubbins"]
53
+ cmd += ["--extension-repo", str(Path.cwd() / "extensions" / "gubbins")]
54
+ else:
55
+ cmd += ["--extension-name", client_extension_name]
56
+ cmd += ["--extension-repo", str(Path.cwd())]
57
+ print("Running:", shlex.join(cmd))
58
+ proc = subprocess.run(cmd, check=False)
59
+ if proc.returncode == 0:
60
+ print("Process completed successfully.")
61
+ else:
62
+ print("Process failed with return code", proc.returncode)
63
+ raise SystemExit(proc.returncode)
64
+
65
+
66
+ if __name__ == "__main__":
67
+ parse_args()
@@ -0,0 +1,235 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = [
4
+ "regenerate_client",
5
+ ]
6
+
7
+ import argparse
8
+ import ast
9
+ import importlib.util
10
+ import json
11
+ import os
12
+ import shlex
13
+ import subprocess
14
+ import sys
15
+ from importlib.metadata import Distribution
16
+ from pathlib import Path
17
+ from urllib.parse import urlparse
18
+
19
+ import git
20
+
21
+ AUTOREST_VERSION = "3.7.1"
22
+ AUTOREST_CORE_VERSION = "3.10.4"
23
+ AUTOREST_PUGINS = {
24
+ "@autorest/python": "6.34.2",
25
+ "@autorest/modelerfour": "4.23.7",
26
+ }
27
+
28
+
29
+ def extract_static_all(path):
30
+ tree = ast.parse(path.read_text(), filename=path)
31
+
32
+ name_to_module = {}
33
+ for node in tree.body:
34
+ if isinstance(node, ast.ImportFrom):
35
+ # Skip wildcard imports (like 'from ... import *')
36
+ for alias in node.names:
37
+ if alias.name == "*":
38
+ continue
39
+ # Use the alias if available, otherwise the original name.
40
+ local_name = alias.asname if alias.asname else alias.name
41
+ name_to_module[local_name] = node.module
42
+
43
+ # Look for the first top-level assignment to __all__
44
+ for node in tree.body:
45
+ if not isinstance(node, ast.Assign):
46
+ continue
47
+ for target in node.targets:
48
+ if not (isinstance(target, ast.Name) and target.id == "__all__"):
49
+ continue
50
+ return {
51
+ name: name_to_module.get(name) for name in ast.literal_eval(node.value)
52
+ }
53
+ raise NotImplementedError("__all__ not found")
54
+
55
+
56
+ def fixup_models_init(generated_dir, extension_name):
57
+ """Workaround for https://github.com/python/mypy/issues/15300."""
58
+ models_init_path = generated_dir / "models" / "__init__.py"
59
+ # Enums cannot be extended, so we don't need to patch them
60
+ object_names = {
61
+ name
62
+ for name, module in extract_static_all(models_init_path).items()
63
+ if module != "_enums"
64
+ }
65
+
66
+ patch_module = "diracx.client._generated.models._patch"
67
+ spec = importlib.util.find_spec(patch_module)
68
+ if spec is None or spec.origin is None:
69
+ raise ImportError(f"Cannot locate {patch_module} package")
70
+ missing = set(extract_static_all(Path(spec.origin))) - set(object_names)
71
+ missing_formatted = "\n".join(f' "{name}",' for name in missing)
72
+
73
+ with models_init_path.open("a") as fh:
74
+ fh.write(
75
+ "if TYPE_CHECKING:\n"
76
+ " __all__.extend(\n"
77
+ " [\n"
78
+ f"{missing_formatted}"
79
+ " ]\n"
80
+ " )\n"
81
+ )
82
+
83
+
84
+ def _module_path(module_name: str) -> Path:
85
+ """Get the path to a module.
86
+
87
+ Args:
88
+ module_name: The name of the module.
89
+
90
+ Returns:
91
+ The path to the module.
92
+
93
+ """
94
+ spec = importlib.util.find_spec(module_name)
95
+ if spec is None:
96
+ raise ImportError("Cannot locate client_module package")
97
+ if spec.origin is None:
98
+ raise ImportError(
99
+ "Cannot locate client_module package, did you forget the __init__.py?"
100
+ )
101
+ return Path(spec.origin).parent
102
+
103
+
104
+ def regenerate_client(openapi_spec: Path, client_module: str):
105
+ """Regenerate the AutoREST client and run pre-commit checks on it.
106
+
107
+ This test is skipped by default, and can be enabled by passing
108
+ --regenerate-client to pytest. It is intended to be run manually
109
+ when the API changes.
110
+
111
+ The reason this is a test is that it is the only way to get access to the
112
+ test_client fixture, which is required to get the OpenAPI spec.
113
+
114
+ WARNING: This test will modify the source code of the client!
115
+ """
116
+ client_root = _module_path(client_module)
117
+
118
+ # If the client is not an editable install, we need to find the source code
119
+ direct_url = Distribution.from_name(client_module).read_text("direct_url.json")
120
+ pkg_url_info = json.loads(direct_url)
121
+ if not pkg_url_info.get("dir_info", {}).get("editable", False):
122
+ src_url = pkg_url_info.get("url")
123
+ if src_url is None:
124
+ raise ValueError("No URL found in direct_url.json")
125
+ url_info = urlparse(src_url)
126
+ if url_info.scheme != "file":
127
+ raise ValueError("URL is not a file URL")
128
+ pkg_root = Path(url_info.path) / "src" / client_module.replace(".", "/")
129
+ if not pkg_root.is_dir():
130
+ raise NotImplementedError(
131
+ "Failed to resolve client sources", client_module, pkg_root
132
+ )
133
+ client_root = pkg_root
134
+
135
+ if not pkg_url_info.get("dir_info", {}).get("editable", False):
136
+ raise NotImplementedError(
137
+ "Client generation from non-editable install is not yet supported",
138
+ )
139
+
140
+ assert client_root.is_dir()
141
+ assert client_root.name == "client"
142
+ assert (client_root / "_generated").is_dir()
143
+ extension_name = client_root.parent.name
144
+
145
+ repo_root = client_root.parents[3]
146
+ if extension_name == "gubbins" and not (repo_root / ".git").is_dir():
147
+ # Gubbins is special because it has a different structure due to being
148
+ # in a subdirectory of diracx
149
+ repo_root = repo_root.parents[1]
150
+ assert (repo_root / ".git").is_dir()
151
+ repo = git.Repo(repo_root)
152
+ generated_dir = client_root / "_generated"
153
+ if repo.is_dirty(path=generated_dir):
154
+ raise AssertionError(
155
+ "Client is currently in a modified state, skipping regeneration"
156
+ )
157
+
158
+ cmd = ["autorest", f"--version={AUTOREST_CORE_VERSION}"]
159
+ for plugin, version in AUTOREST_PUGINS.items():
160
+ cmd.append(f"--use={plugin}@{version}")
161
+ cmd += [
162
+ "--python",
163
+ f"--input-file={openapi_spec}",
164
+ "--models-mode=msrest",
165
+ "--namespace=_generated",
166
+ f"--output-folder={client_root}",
167
+ ]
168
+
169
+ if "AUTOREST_HOME" in os.environ:
170
+ os.makedirs(os.environ["AUTOREST_HOME"], exist_ok=True)
171
+
172
+ # ruff: disable=S603
173
+ subprocess.run(cmd, check=True)
174
+
175
+ if extension_name != "diracx":
176
+ # For now we don't support extending the models in extensions. To make
177
+ # this clear manually remove the automatically generated _patch.py file
178
+ # and fixup the __init__.py file to use the diracx one.
179
+ (generated_dir / "models" / "_patch.py").unlink()
180
+ models_init_path = generated_dir / "models" / "__init__.py"
181
+ models_init = models_init_path.read_text()
182
+ assert models_init.count("from ._patch import") == 4
183
+ models_init = models_init.replace(
184
+ "from ._patch import",
185
+ "from diracx.client._generated.models._patch import",
186
+ )
187
+ models_init_path.write_text(models_init)
188
+
189
+ fixup_models_init(generated_dir, extension_name)
190
+
191
+ cmd = ["pre-commit", "run", "--all-files"]
192
+ print("Running pre-commit...")
193
+ subprocess.run(cmd, check=False, cwd=repo_root)
194
+ print("Re-running pre-commit...")
195
+ proc = subprocess.run(cmd, check=False, cwd=repo_root)
196
+ if proc.returncode == 0 and not repo.is_dirty(path=generated_dir):
197
+ return
198
+ # Show the diff to aid debugging
199
+ print(repo.git.diff(generated_dir))
200
+ if proc.returncode != 0:
201
+ raise AssertionError("Pre-commit failed")
202
+ raise AssertionError("Client was regenerated with changes")
203
+
204
+
205
+ def main():
206
+ from diracx.core.extensions import extensions_by_priority
207
+
208
+ parser = argparse.ArgumentParser(
209
+ description="Regenerate the AutoREST client and run pre-commit checks on it."
210
+ )
211
+ parser.parse_args()
212
+
213
+ client_extension_name = min(extensions_by_priority(), key=lambda x: x == "diracx")
214
+
215
+ cmd = ["npm", "install", "-g", f"autorest@{AUTOREST_VERSION}"]
216
+ print("Ensuring autorest is installed by running", shlex.join(cmd))
217
+ subprocess.run(cmd, check=True)
218
+
219
+ cmd = [
220
+ sys.executable,
221
+ "-m",
222
+ "pytest",
223
+ "-pdiracx.testing",
224
+ "--import-mode=importlib",
225
+ "--no-cov",
226
+ f"--regenerate-client={client_extension_name}",
227
+ str(Path(__file__).parent / "client_generation_pytest.py"),
228
+ ]
229
+ print("Generating client for", client_extension_name)
230
+ print("Running:", shlex.join(cmd))
231
+ subprocess.run(cmd, check=True)
232
+
233
+
234
+ if __name__ == "__main__":
235
+ main()
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from diracx.testing.client_generation import regenerate_client
6
+
7
+ pytestmark = pytest.mark.enabled_dependencies([])
8
+
9
+
10
+ @pytest.fixture
11
+ def test_client(client_factory):
12
+ with client_factory.unauthenticated() as client:
13
+ yield client
14
+
15
+
16
+ def test_regenerate_client(test_client, tmp_path, request):
17
+ """Regenerate the AutoREST client and run pre-commit checks on it.
18
+
19
+ This test is skipped by default, and can be enabled by passing
20
+ --regenerate-client to pytest. It is intended to be run manually
21
+ when the API changes.
22
+
23
+ The reason this is a test is that it is the only way to get access to the
24
+ test_client fixture, which is required to get the OpenAPI spec.
25
+
26
+ WARNING: This test will modify the source code of the client!
27
+ """
28
+ client_name = request.config.getoption("--regenerate-client")
29
+ if client_name is None:
30
+ pytest.skip("--regenerate-client not specified")
31
+
32
+ r = test_client.get("/api/openapi.json")
33
+ r.raise_for_status()
34
+ openapi_spec = tmp_path / "openapi.json"
35
+ openapi_spec.write_text(r.text)
36
+
37
+ regenerate_client(openapi_spec, f"{client_name}.client")
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ import secrets
4
+
5
+ from diracx.db.os.utils import BaseOSDB
6
+
7
+
8
+ class DummyOSDB(BaseOSDB):
9
+ """Example DiracX OpenSearch database class for testing.
10
+
11
+ A new random prefix is created each time the class is defined to ensure
12
+ test runs are independent of each other.
13
+ """
14
+
15
+ fields = {
16
+ "DateField": {"type": "date"},
17
+ "IntField": {"type": "long"},
18
+ "KeywordField0": {"type": "keyword"},
19
+ "KeywordField1": {"type": "keyword"},
20
+ "KeywordField2": {"type": "keyword"},
21
+ "TextField": {"type": "text"},
22
+ }
23
+
24
+ def __init__(self, *args, **kwargs):
25
+ # Randomize the index prefix to ensure tests are independent
26
+ self.index_prefix = f"dummy_{secrets.token_hex(8)}"
27
+ super().__init__(*args, **kwargs)
28
+
29
+ def index_name(self, vo: str, doc_id: int) -> str:
30
+ return f"{self.index_prefix}-{doc_id // 1e6:.0f}m"
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ import tomllib
4
+ from collections import defaultdict
5
+ from importlib.metadata import PackageNotFoundError, distribution, entry_points
6
+
7
+ import pytest
8
+
9
+
10
+ def get_installed_entry_points():
11
+ """Retrieve the installed entry points from the environment."""
12
+ entry_pts = entry_points()
13
+ diracx_eps = defaultdict(dict)
14
+ for group in entry_pts.groups:
15
+ if "diracx" in group:
16
+ for ep in entry_pts.select(group=group):
17
+ diracx_eps[group][ep.name] = ep.value
18
+ return dict(diracx_eps)
19
+
20
+
21
+ def get_entry_points_from_toml(toml_file):
22
+ """Parse entry points from pyproject.toml."""
23
+ with open(toml_file, "rb") as f:
24
+ pyproject = tomllib.load(f)
25
+ package_name = pyproject["project"]["name"]
26
+ return package_name, pyproject.get("project", {}).get("entry-points", {})
27
+
28
+
29
+ def get_current_entry_points(repo_base) -> bool:
30
+ """Create current entry points dict for comparison."""
31
+ current_eps = {}
32
+ for toml_file in repo_base.glob("diracx-*/pyproject.toml"):
33
+ package_name, entry_pts = get_entry_points_from_toml(f"{toml_file}")
34
+ # Ignore packages that are not installed
35
+ try:
36
+ distribution(package_name)
37
+ except PackageNotFoundError:
38
+ continue
39
+ # Merge the entry points
40
+ for key, value in entry_pts.items():
41
+ current_eps[key] = current_eps.get(key, {}) | value
42
+ return current_eps
43
+
44
+
45
+ @pytest.fixture(scope="session", autouse=True)
46
+ def verify_entry_points(request, pytestconfig):
47
+ try:
48
+ ini_toml_name = tomllib.loads(pytestconfig.inipath.read_text())["project"][
49
+ "name"
50
+ ]
51
+ except tomllib.TOMLDecodeError:
52
+ return
53
+ if ini_toml_name == "diracx":
54
+ repo_base = pytestconfig.inipath.parent
55
+ elif ini_toml_name.startswith("diracx-"):
56
+ repo_base = pytestconfig.inipath.parent.parent
57
+ else:
58
+ return
59
+
60
+ installed_eps = set(get_installed_entry_points())
61
+ current_eps = set(get_current_entry_points(repo_base))
62
+
63
+ if installed_eps != current_eps:
64
+ pytest.fail(
65
+ "Project and installed entry-points are not consistent. "
66
+ "You should run `pip install -r requirements-dev.txt`"
67
+ f"{installed_eps-current_eps=}",
68
+ f"{current_eps-installed_eps=}",
69
+ )