llnl-ssapy-data 0.1.0__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.
- llnl_ssapy_data-0.1.0/LICENSE +21 -0
- llnl_ssapy_data-0.1.0/MANIFEST.in +7 -0
- llnl_ssapy_data-0.1.0/NOTICE +7 -0
- llnl_ssapy_data-0.1.0/PKG-INFO +162 -0
- llnl_ssapy_data-0.1.0/README.rst +132 -0
- llnl_ssapy_data-0.1.0/pyproject.toml +53 -0
- llnl_ssapy_data-0.1.0/setup.cfg +4 -0
- llnl_ssapy_data-0.1.0/src/llnl_ssapy_data.egg-info/PKG-INFO +162 -0
- llnl_ssapy_data-0.1.0/src/llnl_ssapy_data.egg-info/SOURCES.txt +16 -0
- llnl_ssapy_data-0.1.0/src/llnl_ssapy_data.egg-info/dependency_links.txt +1 -0
- llnl_ssapy_data-0.1.0/src/llnl_ssapy_data.egg-info/requires.txt +5 -0
- llnl_ssapy_data-0.1.0/src/llnl_ssapy_data.egg-info/top_level.txt +1 -0
- llnl_ssapy_data-0.1.0/src/ssapy_data/__init__.py +30 -0
- llnl_ssapy_data-0.1.0/src/ssapy_data/_manifest.py +23 -0
- llnl_ssapy_data-0.1.0/src/ssapy_data/_resources.py +165 -0
- llnl_ssapy_data-0.1.0/src/ssapy_data/data/README.md +9 -0
- llnl_ssapy_data-0.1.0/src/ssapy_data/manifest.json +12 -0
- llnl_ssapy_data-0.1.0/tests/test_package_data.py +61 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2018, Lawrence Livermore National Security, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
This work was produced under the auspices of the U.S. Department of Energy by Lawrence Livermore National Laboratory under Contract DE-AC52-07NA27344.
|
|
2
|
+
|
|
3
|
+
This work was prepared as an account of work sponsored by an agency of the United States Government. Neither the United States Government nor Lawrence Livermore National Security, LLC, nor any of their employees makes any warranty, expressed or implied, or assumes any legal liability or responsibility for the accuracy, completeness, or usefulness of any information, apparatus, product, or process disclosed, or represents that its use would not infringe privately owned rights.
|
|
4
|
+
|
|
5
|
+
Reference herein to any specific commercial product, process, or service by trade name, trademark, manufacturer, or otherwise does not necessarily constitute or imply its endorsement, recommendation, or favoring by the United States Government or Lawrence Livermore National Security, LLC.
|
|
6
|
+
|
|
7
|
+
The views and opinions of authors expressed herein do not necessarily state or reflect those of the United States Government or Lawrence Livermore National Security, LLC, and shall not be used for advertising or product endorsement purposes.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llnl-ssapy-data
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Packaged data resources for SSAPy and SSAPy Toolkit
|
|
5
|
+
Author-email: LLNL SSAPy Software Team <yeagerastro@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/llnl/SSAPy-Data
|
|
8
|
+
Project-URL: Repository, https://github.com/llnl/SSAPy-Data
|
|
9
|
+
Project-URL: Issues, https://github.com/llnl/SSAPy-Data/issues
|
|
10
|
+
Keywords: ssapy,astrodynamics,orbital-mechanics,data
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/x-rst
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
License-File: NOTICE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest; extra == "dev"
|
|
28
|
+
Requires-Dist: twine; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
SSAPy-Data
|
|
32
|
+
==========
|
|
33
|
+
|
|
34
|
+
SSAPy-Data stores reusable data resources for `SSAPy <https://github.com/llnl/SSAPy>`_
|
|
35
|
+
and `SSAPy Toolkit <https://github.com/llnl/SSAPy-Toolkit>`_. The repository is
|
|
36
|
+
packaged as the ``llnl-ssapy-data`` Python distribution and exposes the
|
|
37
|
+
``ssapy_data`` import package. Data files live under ``src/ssapy_data/data`` so
|
|
38
|
+
users can receive required data through normal ``pip`` installation without Git
|
|
39
|
+
LFS, git submodules, or runtime GitHub downloads.
|
|
40
|
+
|
|
41
|
+
The initial package intentionally does not duplicate data already packaged by
|
|
42
|
+
base SSAPy. New SSAPy Toolkit datasets should be added here when they are needed
|
|
43
|
+
by toolkit functions and are not already available from the base ``llnl-ssapy``
|
|
44
|
+
wheel.
|
|
45
|
+
|
|
46
|
+
Installation
|
|
47
|
+
------------
|
|
48
|
+
|
|
49
|
+
Install from a local clone in editable mode:
|
|
50
|
+
|
|
51
|
+
.. code-block:: bash
|
|
52
|
+
|
|
53
|
+
pip install -e .
|
|
54
|
+
|
|
55
|
+
Build the wheel and source distribution:
|
|
56
|
+
|
|
57
|
+
.. code-block:: bash
|
|
58
|
+
|
|
59
|
+
python -m build
|
|
60
|
+
ls -lh dist/
|
|
61
|
+
|
|
62
|
+
Using packaged data
|
|
63
|
+
-------------------
|
|
64
|
+
|
|
65
|
+
Access packaged data with ``importlib.resources`` helpers exposed by
|
|
66
|
+
``ssapy_data``:
|
|
67
|
+
|
|
68
|
+
.. code-block:: python
|
|
69
|
+
|
|
70
|
+
from ssapy_data import data_path, read_text
|
|
71
|
+
|
|
72
|
+
gravity_header = read_text("egm84.egm")
|
|
73
|
+
|
|
74
|
+
with data_path("Earth_graphics/ne_50m_ocean.shp") as path:
|
|
75
|
+
print(path)
|
|
76
|
+
|
|
77
|
+
``data_path`` yields a real filesystem path for libraries that require paths.
|
|
78
|
+
Use the path only inside the context manager because zipped wheels may extract
|
|
79
|
+
resources to temporary locations.
|
|
80
|
+
|
|
81
|
+
Adding data
|
|
82
|
+
-----------
|
|
83
|
+
|
|
84
|
+
Add new reusable data below ``src/ssapy_data/data``. Preserve source filenames
|
|
85
|
+
when possible, and use subdirectories when a dataset has multiple sidecar files.
|
|
86
|
+
Do not add files already packaged by base SSAPy unless a later migration
|
|
87
|
+
explicitly moves that dependency here.
|
|
88
|
+
After adding, replacing, or removing data, regenerate the manifest:
|
|
89
|
+
|
|
90
|
+
.. code-block:: bash
|
|
91
|
+
|
|
92
|
+
python scripts/update_manifest.py
|
|
93
|
+
python -m pytest
|
|
94
|
+
python -m build
|
|
95
|
+
|
|
96
|
+
The manifest records each packaged file path, byte count, and SHA-256 digest in
|
|
97
|
+
``src/ssapy_data/manifest.json``. Pull requests that change data should also
|
|
98
|
+
update the source/provenance notes in this README when the dataset source or
|
|
99
|
+
license differs from the existing entries.
|
|
100
|
+
|
|
101
|
+
Size guidance
|
|
102
|
+
-------------
|
|
103
|
+
|
|
104
|
+
The initial wheel contains only the package helpers and a data-directory README.
|
|
105
|
+
Before adding large datasets, estimate the built wheel size with:
|
|
106
|
+
|
|
107
|
+
.. code-block:: bash
|
|
108
|
+
|
|
109
|
+
python -m build --wheel
|
|
110
|
+
ls -lh dist/*.whl
|
|
111
|
+
|
|
112
|
+
If a future dataset pushes the wheel above PyPI limits, split the data into a
|
|
113
|
+
separate companion package rather than using Git LFS in SSAPy Toolkit.
|
|
114
|
+
|
|
115
|
+
Publishing
|
|
116
|
+
----------
|
|
117
|
+
|
|
118
|
+
The repository publishes ``llnl-ssapy-data`` to PyPI through GitHub Actions and
|
|
119
|
+
PyPI trusted publishing. Configure PyPI before creating the first release:
|
|
120
|
+
|
|
121
|
+
* Create a PyPI trusted publisher, or pending publisher, for project
|
|
122
|
+
``llnl-ssapy-data``.
|
|
123
|
+
* Set the owner to ``llnl`` and repository to ``SSAPy-Data``.
|
|
124
|
+
* Set the workflow filename to ``publish.yml``.
|
|
125
|
+
* Set the GitHub environment to ``pypi``.
|
|
126
|
+
|
|
127
|
+
After PyPI trust is configured, publish by creating a GitHub release whose tag
|
|
128
|
+
matches the version in ``pyproject.toml``, for example ``v0.1.0``. The
|
|
129
|
+
``Publish to PyPI`` workflow builds a clean wheel and source distribution, runs
|
|
130
|
+
tests, checks the manifest, and uploads through OpenID Connect (OIDC). No PyPI
|
|
131
|
+
API token is required.
|
|
132
|
+
|
|
133
|
+
Data provenance
|
|
134
|
+
---------------
|
|
135
|
+
|
|
136
|
+
Each data pull request should document the source URL, license, retrieval date,
|
|
137
|
+
and any preprocessing steps for new packaged datasets. Candidate sources include:
|
|
138
|
+
|
|
139
|
+
* Earth gravity fields:
|
|
140
|
+
`ICGEM time-variable gravity fields <http://icgem.gfz-potsdam.de/tom_longtime>`_
|
|
141
|
+
* Other celestial bodies:
|
|
142
|
+
`ICGEM celestial gravity fields <http://icgem.gfz-potsdam.de/tom_celestial>`_
|
|
143
|
+
|
|
144
|
+
Code of Conduct
|
|
145
|
+
---------------
|
|
146
|
+
|
|
147
|
+
Please note that SSAPy-Data has a
|
|
148
|
+
`Code of Conduct <https://github.com/LLNL/SSAPy-Data/blob/main/CODE_OF_CONDUCT.md>`_.
|
|
149
|
+
By participating in the SSAPy-Data community, you agree to abide by its rules.
|
|
150
|
+
|
|
151
|
+
License
|
|
152
|
+
-------
|
|
153
|
+
|
|
154
|
+
SSAPy-Data is distributed under the terms of the MIT license. All new
|
|
155
|
+
contributions must be made under the MIT license.
|
|
156
|
+
|
|
157
|
+
See the `license <https://github.com/LLNL/SSAPy-Data/blob/main/LICENSE>`_ and
|
|
158
|
+
`NOTICE <https://github.com/LLNL/SSAPy-Data/blob/main/NOTICE>`_ for details.
|
|
159
|
+
|
|
160
|
+
SPDX-License-Identifier: MIT
|
|
161
|
+
|
|
162
|
+
LLNL-CODE-862420
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
SSAPy-Data
|
|
2
|
+
==========
|
|
3
|
+
|
|
4
|
+
SSAPy-Data stores reusable data resources for `SSAPy <https://github.com/llnl/SSAPy>`_
|
|
5
|
+
and `SSAPy Toolkit <https://github.com/llnl/SSAPy-Toolkit>`_. The repository is
|
|
6
|
+
packaged as the ``llnl-ssapy-data`` Python distribution and exposes the
|
|
7
|
+
``ssapy_data`` import package. Data files live under ``src/ssapy_data/data`` so
|
|
8
|
+
users can receive required data through normal ``pip`` installation without Git
|
|
9
|
+
LFS, git submodules, or runtime GitHub downloads.
|
|
10
|
+
|
|
11
|
+
The initial package intentionally does not duplicate data already packaged by
|
|
12
|
+
base SSAPy. New SSAPy Toolkit datasets should be added here when they are needed
|
|
13
|
+
by toolkit functions and are not already available from the base ``llnl-ssapy``
|
|
14
|
+
wheel.
|
|
15
|
+
|
|
16
|
+
Installation
|
|
17
|
+
------------
|
|
18
|
+
|
|
19
|
+
Install from a local clone in editable mode:
|
|
20
|
+
|
|
21
|
+
.. code-block:: bash
|
|
22
|
+
|
|
23
|
+
pip install -e .
|
|
24
|
+
|
|
25
|
+
Build the wheel and source distribution:
|
|
26
|
+
|
|
27
|
+
.. code-block:: bash
|
|
28
|
+
|
|
29
|
+
python -m build
|
|
30
|
+
ls -lh dist/
|
|
31
|
+
|
|
32
|
+
Using packaged data
|
|
33
|
+
-------------------
|
|
34
|
+
|
|
35
|
+
Access packaged data with ``importlib.resources`` helpers exposed by
|
|
36
|
+
``ssapy_data``:
|
|
37
|
+
|
|
38
|
+
.. code-block:: python
|
|
39
|
+
|
|
40
|
+
from ssapy_data import data_path, read_text
|
|
41
|
+
|
|
42
|
+
gravity_header = read_text("egm84.egm")
|
|
43
|
+
|
|
44
|
+
with data_path("Earth_graphics/ne_50m_ocean.shp") as path:
|
|
45
|
+
print(path)
|
|
46
|
+
|
|
47
|
+
``data_path`` yields a real filesystem path for libraries that require paths.
|
|
48
|
+
Use the path only inside the context manager because zipped wheels may extract
|
|
49
|
+
resources to temporary locations.
|
|
50
|
+
|
|
51
|
+
Adding data
|
|
52
|
+
-----------
|
|
53
|
+
|
|
54
|
+
Add new reusable data below ``src/ssapy_data/data``. Preserve source filenames
|
|
55
|
+
when possible, and use subdirectories when a dataset has multiple sidecar files.
|
|
56
|
+
Do not add files already packaged by base SSAPy unless a later migration
|
|
57
|
+
explicitly moves that dependency here.
|
|
58
|
+
After adding, replacing, or removing data, regenerate the manifest:
|
|
59
|
+
|
|
60
|
+
.. code-block:: bash
|
|
61
|
+
|
|
62
|
+
python scripts/update_manifest.py
|
|
63
|
+
python -m pytest
|
|
64
|
+
python -m build
|
|
65
|
+
|
|
66
|
+
The manifest records each packaged file path, byte count, and SHA-256 digest in
|
|
67
|
+
``src/ssapy_data/manifest.json``. Pull requests that change data should also
|
|
68
|
+
update the source/provenance notes in this README when the dataset source or
|
|
69
|
+
license differs from the existing entries.
|
|
70
|
+
|
|
71
|
+
Size guidance
|
|
72
|
+
-------------
|
|
73
|
+
|
|
74
|
+
The initial wheel contains only the package helpers and a data-directory README.
|
|
75
|
+
Before adding large datasets, estimate the built wheel size with:
|
|
76
|
+
|
|
77
|
+
.. code-block:: bash
|
|
78
|
+
|
|
79
|
+
python -m build --wheel
|
|
80
|
+
ls -lh dist/*.whl
|
|
81
|
+
|
|
82
|
+
If a future dataset pushes the wheel above PyPI limits, split the data into a
|
|
83
|
+
separate companion package rather than using Git LFS in SSAPy Toolkit.
|
|
84
|
+
|
|
85
|
+
Publishing
|
|
86
|
+
----------
|
|
87
|
+
|
|
88
|
+
The repository publishes ``llnl-ssapy-data`` to PyPI through GitHub Actions and
|
|
89
|
+
PyPI trusted publishing. Configure PyPI before creating the first release:
|
|
90
|
+
|
|
91
|
+
* Create a PyPI trusted publisher, or pending publisher, for project
|
|
92
|
+
``llnl-ssapy-data``.
|
|
93
|
+
* Set the owner to ``llnl`` and repository to ``SSAPy-Data``.
|
|
94
|
+
* Set the workflow filename to ``publish.yml``.
|
|
95
|
+
* Set the GitHub environment to ``pypi``.
|
|
96
|
+
|
|
97
|
+
After PyPI trust is configured, publish by creating a GitHub release whose tag
|
|
98
|
+
matches the version in ``pyproject.toml``, for example ``v0.1.0``. The
|
|
99
|
+
``Publish to PyPI`` workflow builds a clean wheel and source distribution, runs
|
|
100
|
+
tests, checks the manifest, and uploads through OpenID Connect (OIDC). No PyPI
|
|
101
|
+
API token is required.
|
|
102
|
+
|
|
103
|
+
Data provenance
|
|
104
|
+
---------------
|
|
105
|
+
|
|
106
|
+
Each data pull request should document the source URL, license, retrieval date,
|
|
107
|
+
and any preprocessing steps for new packaged datasets. Candidate sources include:
|
|
108
|
+
|
|
109
|
+
* Earth gravity fields:
|
|
110
|
+
`ICGEM time-variable gravity fields <http://icgem.gfz-potsdam.de/tom_longtime>`_
|
|
111
|
+
* Other celestial bodies:
|
|
112
|
+
`ICGEM celestial gravity fields <http://icgem.gfz-potsdam.de/tom_celestial>`_
|
|
113
|
+
|
|
114
|
+
Code of Conduct
|
|
115
|
+
---------------
|
|
116
|
+
|
|
117
|
+
Please note that SSAPy-Data has a
|
|
118
|
+
`Code of Conduct <https://github.com/LLNL/SSAPy-Data/blob/main/CODE_OF_CONDUCT.md>`_.
|
|
119
|
+
By participating in the SSAPy-Data community, you agree to abide by its rules.
|
|
120
|
+
|
|
121
|
+
License
|
|
122
|
+
-------
|
|
123
|
+
|
|
124
|
+
SSAPy-Data is distributed under the terms of the MIT license. All new
|
|
125
|
+
contributions must be made under the MIT license.
|
|
126
|
+
|
|
127
|
+
See the `license <https://github.com/LLNL/SSAPy-Data/blob/main/LICENSE>`_ and
|
|
128
|
+
`NOTICE <https://github.com/LLNL/SSAPy-Data/blob/main/NOTICE>`_ for details.
|
|
129
|
+
|
|
130
|
+
SPDX-License-Identifier: MIT
|
|
131
|
+
|
|
132
|
+
LLNL-CODE-862420
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "llnl-ssapy-data"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Packaged data resources for SSAPy and SSAPy Toolkit"
|
|
9
|
+
readme = "README.rst"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE", "NOTICE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "LLNL SSAPy Software Team", email = "yeagerastro@gmail.com" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["ssapy", "astrodynamics", "orbital-mechanics", "data"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Scientific/Engineering :: Astronomy",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"build",
|
|
35
|
+
"pytest",
|
|
36
|
+
"twine",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/llnl/SSAPy-Data"
|
|
41
|
+
Repository = "https://github.com/llnl/SSAPy-Data"
|
|
42
|
+
Issues = "https://github.com/llnl/SSAPy-Data/issues"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools]
|
|
45
|
+
package-dir = { "" = "src" }
|
|
46
|
+
packages = { find = { where = ["src"] } }
|
|
47
|
+
include-package-data = true
|
|
48
|
+
|
|
49
|
+
[tool.setuptools.package-data]
|
|
50
|
+
ssapy_data = ["manifest.json", "data/*", "data/**/*"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llnl-ssapy-data
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Packaged data resources for SSAPy and SSAPy Toolkit
|
|
5
|
+
Author-email: LLNL SSAPy Software Team <yeagerastro@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/llnl/SSAPy-Data
|
|
8
|
+
Project-URL: Repository, https://github.com/llnl/SSAPy-Data
|
|
9
|
+
Project-URL: Issues, https://github.com/llnl/SSAPy-Data/issues
|
|
10
|
+
Keywords: ssapy,astrodynamics,orbital-mechanics,data
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/x-rst
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
License-File: NOTICE
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest; extra == "dev"
|
|
28
|
+
Requires-Dist: twine; extra == "dev"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
SSAPy-Data
|
|
32
|
+
==========
|
|
33
|
+
|
|
34
|
+
SSAPy-Data stores reusable data resources for `SSAPy <https://github.com/llnl/SSAPy>`_
|
|
35
|
+
and `SSAPy Toolkit <https://github.com/llnl/SSAPy-Toolkit>`_. The repository is
|
|
36
|
+
packaged as the ``llnl-ssapy-data`` Python distribution and exposes the
|
|
37
|
+
``ssapy_data`` import package. Data files live under ``src/ssapy_data/data`` so
|
|
38
|
+
users can receive required data through normal ``pip`` installation without Git
|
|
39
|
+
LFS, git submodules, or runtime GitHub downloads.
|
|
40
|
+
|
|
41
|
+
The initial package intentionally does not duplicate data already packaged by
|
|
42
|
+
base SSAPy. New SSAPy Toolkit datasets should be added here when they are needed
|
|
43
|
+
by toolkit functions and are not already available from the base ``llnl-ssapy``
|
|
44
|
+
wheel.
|
|
45
|
+
|
|
46
|
+
Installation
|
|
47
|
+
------------
|
|
48
|
+
|
|
49
|
+
Install from a local clone in editable mode:
|
|
50
|
+
|
|
51
|
+
.. code-block:: bash
|
|
52
|
+
|
|
53
|
+
pip install -e .
|
|
54
|
+
|
|
55
|
+
Build the wheel and source distribution:
|
|
56
|
+
|
|
57
|
+
.. code-block:: bash
|
|
58
|
+
|
|
59
|
+
python -m build
|
|
60
|
+
ls -lh dist/
|
|
61
|
+
|
|
62
|
+
Using packaged data
|
|
63
|
+
-------------------
|
|
64
|
+
|
|
65
|
+
Access packaged data with ``importlib.resources`` helpers exposed by
|
|
66
|
+
``ssapy_data``:
|
|
67
|
+
|
|
68
|
+
.. code-block:: python
|
|
69
|
+
|
|
70
|
+
from ssapy_data import data_path, read_text
|
|
71
|
+
|
|
72
|
+
gravity_header = read_text("egm84.egm")
|
|
73
|
+
|
|
74
|
+
with data_path("Earth_graphics/ne_50m_ocean.shp") as path:
|
|
75
|
+
print(path)
|
|
76
|
+
|
|
77
|
+
``data_path`` yields a real filesystem path for libraries that require paths.
|
|
78
|
+
Use the path only inside the context manager because zipped wheels may extract
|
|
79
|
+
resources to temporary locations.
|
|
80
|
+
|
|
81
|
+
Adding data
|
|
82
|
+
-----------
|
|
83
|
+
|
|
84
|
+
Add new reusable data below ``src/ssapy_data/data``. Preserve source filenames
|
|
85
|
+
when possible, and use subdirectories when a dataset has multiple sidecar files.
|
|
86
|
+
Do not add files already packaged by base SSAPy unless a later migration
|
|
87
|
+
explicitly moves that dependency here.
|
|
88
|
+
After adding, replacing, or removing data, regenerate the manifest:
|
|
89
|
+
|
|
90
|
+
.. code-block:: bash
|
|
91
|
+
|
|
92
|
+
python scripts/update_manifest.py
|
|
93
|
+
python -m pytest
|
|
94
|
+
python -m build
|
|
95
|
+
|
|
96
|
+
The manifest records each packaged file path, byte count, and SHA-256 digest in
|
|
97
|
+
``src/ssapy_data/manifest.json``. Pull requests that change data should also
|
|
98
|
+
update the source/provenance notes in this README when the dataset source or
|
|
99
|
+
license differs from the existing entries.
|
|
100
|
+
|
|
101
|
+
Size guidance
|
|
102
|
+
-------------
|
|
103
|
+
|
|
104
|
+
The initial wheel contains only the package helpers and a data-directory README.
|
|
105
|
+
Before adding large datasets, estimate the built wheel size with:
|
|
106
|
+
|
|
107
|
+
.. code-block:: bash
|
|
108
|
+
|
|
109
|
+
python -m build --wheel
|
|
110
|
+
ls -lh dist/*.whl
|
|
111
|
+
|
|
112
|
+
If a future dataset pushes the wheel above PyPI limits, split the data into a
|
|
113
|
+
separate companion package rather than using Git LFS in SSAPy Toolkit.
|
|
114
|
+
|
|
115
|
+
Publishing
|
|
116
|
+
----------
|
|
117
|
+
|
|
118
|
+
The repository publishes ``llnl-ssapy-data`` to PyPI through GitHub Actions and
|
|
119
|
+
PyPI trusted publishing. Configure PyPI before creating the first release:
|
|
120
|
+
|
|
121
|
+
* Create a PyPI trusted publisher, or pending publisher, for project
|
|
122
|
+
``llnl-ssapy-data``.
|
|
123
|
+
* Set the owner to ``llnl`` and repository to ``SSAPy-Data``.
|
|
124
|
+
* Set the workflow filename to ``publish.yml``.
|
|
125
|
+
* Set the GitHub environment to ``pypi``.
|
|
126
|
+
|
|
127
|
+
After PyPI trust is configured, publish by creating a GitHub release whose tag
|
|
128
|
+
matches the version in ``pyproject.toml``, for example ``v0.1.0``. The
|
|
129
|
+
``Publish to PyPI`` workflow builds a clean wheel and source distribution, runs
|
|
130
|
+
tests, checks the manifest, and uploads through OpenID Connect (OIDC). No PyPI
|
|
131
|
+
API token is required.
|
|
132
|
+
|
|
133
|
+
Data provenance
|
|
134
|
+
---------------
|
|
135
|
+
|
|
136
|
+
Each data pull request should document the source URL, license, retrieval date,
|
|
137
|
+
and any preprocessing steps for new packaged datasets. Candidate sources include:
|
|
138
|
+
|
|
139
|
+
* Earth gravity fields:
|
|
140
|
+
`ICGEM time-variable gravity fields <http://icgem.gfz-potsdam.de/tom_longtime>`_
|
|
141
|
+
* Other celestial bodies:
|
|
142
|
+
`ICGEM celestial gravity fields <http://icgem.gfz-potsdam.de/tom_celestial>`_
|
|
143
|
+
|
|
144
|
+
Code of Conduct
|
|
145
|
+
---------------
|
|
146
|
+
|
|
147
|
+
Please note that SSAPy-Data has a
|
|
148
|
+
`Code of Conduct <https://github.com/LLNL/SSAPy-Data/blob/main/CODE_OF_CONDUCT.md>`_.
|
|
149
|
+
By participating in the SSAPy-Data community, you agree to abide by its rules.
|
|
150
|
+
|
|
151
|
+
License
|
|
152
|
+
-------
|
|
153
|
+
|
|
154
|
+
SSAPy-Data is distributed under the terms of the MIT license. All new
|
|
155
|
+
contributions must be made under the MIT license.
|
|
156
|
+
|
|
157
|
+
See the `license <https://github.com/LLNL/SSAPy-Data/blob/main/LICENSE>`_ and
|
|
158
|
+
`NOTICE <https://github.com/LLNL/SSAPy-Data/blob/main/NOTICE>`_ for details.
|
|
159
|
+
|
|
160
|
+
SPDX-License-Identifier: MIT
|
|
161
|
+
|
|
162
|
+
LLNL-CODE-862420
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
NOTICE
|
|
4
|
+
README.rst
|
|
5
|
+
pyproject.toml
|
|
6
|
+
src/llnl_ssapy_data.egg-info/PKG-INFO
|
|
7
|
+
src/llnl_ssapy_data.egg-info/SOURCES.txt
|
|
8
|
+
src/llnl_ssapy_data.egg-info/dependency_links.txt
|
|
9
|
+
src/llnl_ssapy_data.egg-info/requires.txt
|
|
10
|
+
src/llnl_ssapy_data.egg-info/top_level.txt
|
|
11
|
+
src/ssapy_data/__init__.py
|
|
12
|
+
src/ssapy_data/_manifest.py
|
|
13
|
+
src/ssapy_data/_resources.py
|
|
14
|
+
src/ssapy_data/manifest.json
|
|
15
|
+
src/ssapy_data/data/README.md
|
|
16
|
+
tests/test_package_data.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ssapy_data
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Packaged data resources for SSAPy and SSAPy Toolkit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ._manifest import manifest, manifest_entries
|
|
6
|
+
from ._resources import (
|
|
7
|
+
DataResourceNotFoundError,
|
|
8
|
+
data_path,
|
|
9
|
+
data_resource,
|
|
10
|
+
iter_data_files,
|
|
11
|
+
open_binary,
|
|
12
|
+
open_text,
|
|
13
|
+
read_binary,
|
|
14
|
+
read_text,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"DataResourceNotFoundError",
|
|
21
|
+
"data_path",
|
|
22
|
+
"data_resource",
|
|
23
|
+
"iter_data_files",
|
|
24
|
+
"manifest",
|
|
25
|
+
"manifest_entries",
|
|
26
|
+
"open_binary",
|
|
27
|
+
"open_text",
|
|
28
|
+
"read_binary",
|
|
29
|
+
"read_text",
|
|
30
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Manifest helpers for packaged SSAPy data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from importlib.resources import files
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
PACKAGE = "ssapy_data"
|
|
10
|
+
MANIFEST_NAME = "manifest.json"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def manifest() -> dict[str, Any]:
|
|
14
|
+
"""Return the packaged data manifest."""
|
|
15
|
+
|
|
16
|
+
resource = files(PACKAGE).joinpath(MANIFEST_NAME)
|
|
17
|
+
return json.loads(resource.read_text(encoding="utf-8"))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def manifest_entries() -> list[dict[str, Any]]:
|
|
21
|
+
"""Return file entries from the packaged data manifest."""
|
|
22
|
+
|
|
23
|
+
return list(manifest()["files"])
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Resource helpers for data packaged in ``ssapy_data`` wheels."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from importlib.resources import as_file, files
|
|
7
|
+
try:
|
|
8
|
+
from importlib.resources.abc import Traversable
|
|
9
|
+
except ImportError: # Python 3.10
|
|
10
|
+
from importlib.abc import Traversable
|
|
11
|
+
from os import PathLike
|
|
12
|
+
from pathlib import Path, PurePosixPath
|
|
13
|
+
from typing import Iterator
|
|
14
|
+
|
|
15
|
+
DATA_ROOT = "data"
|
|
16
|
+
PACKAGE = "ssapy_data"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DataResourceNotFoundError(FileNotFoundError):
|
|
20
|
+
"""Raised when a requested packaged data resource is absent."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def data_resource(
|
|
24
|
+
relative_path: str | PathLike[str] = "",
|
|
25
|
+
*,
|
|
26
|
+
data_root: str | PathLike[str] = DATA_ROOT,
|
|
27
|
+
must_exist: bool = True,
|
|
28
|
+
) -> Traversable:
|
|
29
|
+
"""Return an ``importlib.resources`` object for packaged SSAPy data.
|
|
30
|
+
|
|
31
|
+
Parameters
|
|
32
|
+
----------
|
|
33
|
+
relative_path
|
|
34
|
+
POSIX-style path below ``data_root``. Absolute paths and ``..`` traversal
|
|
35
|
+
are rejected.
|
|
36
|
+
data_root
|
|
37
|
+
Directory inside ``ssapy_data`` that contains data resources.
|
|
38
|
+
must_exist
|
|
39
|
+
If ``True``, raise :class:`DataResourceNotFoundError` when the resource
|
|
40
|
+
is missing.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
resource = files(PACKAGE)
|
|
44
|
+
for part in _safe_parts(data_root):
|
|
45
|
+
resource = resource.joinpath(part)
|
|
46
|
+
for part in _safe_parts(relative_path):
|
|
47
|
+
resource = resource.joinpath(part)
|
|
48
|
+
|
|
49
|
+
if must_exist and not resource.exists():
|
|
50
|
+
requested = _display_path(data_root, relative_path)
|
|
51
|
+
raise DataResourceNotFoundError(
|
|
52
|
+
f"Data resource '{requested}' was not found in package '{PACKAGE}'."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return resource
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@contextmanager
|
|
59
|
+
def data_path(
|
|
60
|
+
relative_path: str | PathLike[str],
|
|
61
|
+
*,
|
|
62
|
+
data_root: str | PathLike[str] = DATA_ROOT,
|
|
63
|
+
) -> Iterator[Path]:
|
|
64
|
+
"""Yield a filesystem path for a packaged data file.
|
|
65
|
+
|
|
66
|
+
Use this when a downstream library requires a real path instead of a file
|
|
67
|
+
object. The yielded path may be a temporary extraction path for zipped wheels,
|
|
68
|
+
so callers should use it only inside the context manager.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
resource = data_resource(relative_path, data_root=data_root)
|
|
72
|
+
if not resource.is_file():
|
|
73
|
+
requested = _display_path(data_root, relative_path)
|
|
74
|
+
raise DataResourceNotFoundError(f"Data resource '{requested}' is not a file.")
|
|
75
|
+
|
|
76
|
+
with as_file(resource) as path:
|
|
77
|
+
yield path
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@contextmanager
|
|
81
|
+
def open_binary(relative_path: str | PathLike[str], *, data_root: str | PathLike[str] = DATA_ROOT):
|
|
82
|
+
"""Open a packaged data file in binary mode."""
|
|
83
|
+
|
|
84
|
+
resource = data_resource(relative_path, data_root=data_root)
|
|
85
|
+
if not resource.is_file():
|
|
86
|
+
requested = _display_path(data_root, relative_path)
|
|
87
|
+
raise DataResourceNotFoundError(f"Data resource '{requested}' is not a file.")
|
|
88
|
+
|
|
89
|
+
with resource.open("rb") as file_handle:
|
|
90
|
+
yield file_handle
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@contextmanager
|
|
94
|
+
def open_text(
|
|
95
|
+
relative_path: str | PathLike[str],
|
|
96
|
+
*,
|
|
97
|
+
data_root: str | PathLike[str] = DATA_ROOT,
|
|
98
|
+
encoding: str = "utf-8",
|
|
99
|
+
):
|
|
100
|
+
"""Open a packaged data file in text mode."""
|
|
101
|
+
|
|
102
|
+
resource = data_resource(relative_path, data_root=data_root)
|
|
103
|
+
if not resource.is_file():
|
|
104
|
+
requested = _display_path(data_root, relative_path)
|
|
105
|
+
raise DataResourceNotFoundError(f"Data resource '{requested}' is not a file.")
|
|
106
|
+
|
|
107
|
+
with resource.open("r", encoding=encoding) as file_handle:
|
|
108
|
+
yield file_handle
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def read_binary(relative_path: str | PathLike[str], *, data_root: str | PathLike[str] = DATA_ROOT) -> bytes:
|
|
112
|
+
"""Read a packaged binary data file."""
|
|
113
|
+
|
|
114
|
+
with open_binary(relative_path, data_root=data_root) as file_handle:
|
|
115
|
+
return file_handle.read()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def read_text(
|
|
119
|
+
relative_path: str | PathLike[str],
|
|
120
|
+
*,
|
|
121
|
+
data_root: str | PathLike[str] = DATA_ROOT,
|
|
122
|
+
encoding: str = "utf-8",
|
|
123
|
+
) -> str:
|
|
124
|
+
"""Read a packaged text data file."""
|
|
125
|
+
|
|
126
|
+
with open_text(relative_path, data_root=data_root, encoding=encoding) as file_handle:
|
|
127
|
+
return file_handle.read()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def iter_data_files(relative_path: str | PathLike[str] = "") -> Iterator[Traversable]:
|
|
131
|
+
"""Yield packaged data files recursively below ``relative_path``."""
|
|
132
|
+
|
|
133
|
+
root = data_resource(relative_path)
|
|
134
|
+
if root.is_file():
|
|
135
|
+
yield root
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
for child in sorted(root.iterdir(), key=lambda item: item.name):
|
|
139
|
+
if child.is_file():
|
|
140
|
+
yield child
|
|
141
|
+
elif child.is_dir():
|
|
142
|
+
child_relative = _display_path(relative_path, child.name)
|
|
143
|
+
yield from iter_data_files(child_relative)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _safe_parts(path: str | PathLike[str]) -> tuple[str, ...]:
|
|
147
|
+
path_string = str(path)
|
|
148
|
+
if path_string in {"", "."}:
|
|
149
|
+
return ()
|
|
150
|
+
|
|
151
|
+
pure_path = PurePosixPath(path_string)
|
|
152
|
+
if pure_path.is_absolute():
|
|
153
|
+
raise ValueError(f"Packaged data paths must be relative, got '{path_string}'.")
|
|
154
|
+
|
|
155
|
+
parts = tuple(part for part in pure_path.parts if part not in {"", "."})
|
|
156
|
+
if ".." in parts:
|
|
157
|
+
raise ValueError(f"Packaged data paths cannot contain '..', got '{path_string}'.")
|
|
158
|
+
return parts
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _display_path(*paths: str | PathLike[str]) -> str:
|
|
162
|
+
parts: list[str] = []
|
|
163
|
+
for path in paths:
|
|
164
|
+
parts.extend(_safe_parts(path))
|
|
165
|
+
return "/".join(parts)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# SSAPy Data Payload Directory
|
|
2
|
+
|
|
3
|
+
Add new reusable datasets for SSAPy Toolkit under this directory. Do not add data already packaged by base SSAPy unless a later migration explicitly moves that dependency here.
|
|
4
|
+
|
|
5
|
+
After changing files here, run:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python scripts/update_manifest.py
|
|
9
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from ssapy_data import (
|
|
9
|
+
DataResourceNotFoundError,
|
|
10
|
+
data_path,
|
|
11
|
+
data_resource,
|
|
12
|
+
iter_data_files,
|
|
13
|
+
manifest,
|
|
14
|
+
read_binary,
|
|
15
|
+
read_text,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_known_resources_are_packaged():
|
|
20
|
+
assert data_resource("README.md").is_file()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_text_and_binary_read_helpers():
|
|
24
|
+
text = read_text("README.md")
|
|
25
|
+
assert "SSAPy Data Payload Directory" in text
|
|
26
|
+
assert read_binary("README.md").startswith(b"# SSAPy Data")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_data_path_yields_filesystem_path():
|
|
30
|
+
with data_path("README.md") as path:
|
|
31
|
+
assert path.exists()
|
|
32
|
+
assert path.name == "README.md"
|
|
33
|
+
assert path.stat().st_size > 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def test_manifest_matches_packaged_files():
|
|
37
|
+
payload = manifest()
|
|
38
|
+
entries = payload["files"]
|
|
39
|
+
entry_paths = {entry["path"] for entry in entries}
|
|
40
|
+
actual_paths = {resource.name for resource in iter_data_files()}
|
|
41
|
+
|
|
42
|
+
assert payload["schema_version"] == 1
|
|
43
|
+
assert payload["data_root"] == "data"
|
|
44
|
+
assert entry_paths == {"README.md"}
|
|
45
|
+
assert actual_paths == {"README.md"}
|
|
46
|
+
|
|
47
|
+
for entry in entries:
|
|
48
|
+
resource = data_resource(entry["path"])
|
|
49
|
+
with data_path(entry["path"]) as path:
|
|
50
|
+
assert path.stat().st_size == entry["bytes"]
|
|
51
|
+
assert hashlib.sha256(path.read_bytes()).hexdigest() == entry["sha256"]
|
|
52
|
+
assert resource.is_file()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_missing_and_unsafe_paths_are_rejected():
|
|
56
|
+
with pytest.raises(DataResourceNotFoundError, match="missing.dat"):
|
|
57
|
+
data_resource("missing.dat")
|
|
58
|
+
with pytest.raises(ValueError, match="must be relative"):
|
|
59
|
+
data_resource("/earth.png")
|
|
60
|
+
with pytest.raises(ValueError, match="cannot contain"):
|
|
61
|
+
data_resource("Earth_graphics/../earth.png")
|