Skip to content

Commit 36bd2dc

Browse files
Merge pull request #26 from DACCS-Climate/devops-enforcement
add devops tools
2 parents 7d32e3b + 6f7f9c8 commit 36bd2dc

File tree

15 files changed

+230
-67
lines changed

15 files changed

+230
-67
lines changed

.github/dependabot.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Please see the documentation for all configuration options:
2+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
3+
4+
version: 2
5+
updates:
6+
- package-ecosystem: "pip"
7+
directory: "/"
8+
schedule:
9+
interval: "weekly"

.github/workflows/precommit.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: pre-commit
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened, ready_for_review]
6+
7+
jobs:
8+
pre-commit:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
- uses: actions/setup-python@v3
13+
- uses: pre-commit/[email protected]

.pre-commit-config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
# Ruff version.
4+
rev: v0.9.5
5+
hooks:
6+
# Run the linter.
7+
- id: ruff
8+
# Run the formatter.
9+
- id: ruff-format
10+

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,43 @@ access the resource if you have permission:
151151
```python
152152
>>> session.get(f"{client.this_node.url}/some/protected/subpath")
153153
```
154+
155+
## Contributing
156+
157+
We welcome any contributions to this codebase. To submit suggested changes, please do the following:
158+
159+
- create a new feature branch off of `main`
160+
- update the code, write/update tests, write/update documentation
161+
- submit a pull request targetting the `main` branch
162+
163+
To develop this project locally, first clone this repository and install the testing and development
164+
dependencies:
165+
166+
```sh
167+
pip install -e .[dev,test]
168+
```
169+
170+
### Testing
171+
172+
Tests are located in the `tests/` folder can be run using `pytest`:
173+
174+
```sh
175+
pytest tests/
176+
```
177+
178+
### Coding Style
179+
180+
This codebase uses the [`ruff`](https://docs.astral.sh/ruff/) formatter and linter to enforce style policies.
181+
182+
To check that your changes conform to these policies please run:
183+
184+
```sh
185+
ruff format
186+
ruff check
187+
```
188+
189+
You can also set up pre-commit hooks that will run these checks before you create any commit in this repo:
190+
191+
```sh
192+
pre-commit install
193+
```

marble_client/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
from .client import MarbleClient
2-
from .exceptions import MarbleBaseError, ServiceNotAvailableError, UnknownNodeError, JupyterEnvironmentError
2+
from .exceptions import JupyterEnvironmentError, MarbleBaseError, ServiceNotAvailableError, UnknownNodeError
33
from .node import MarbleNode
44
from .services import MarbleService
5+
6+
__all__ = [
7+
"MarbleClient",
8+
"JupyterEnvironmentError",
9+
"MarbleBaseError",
10+
"ServiceNotAvailableError",
11+
"UnknownNodeError",
12+
"MarbleNode",
13+
"MarbleService",
14+
]

marble_client/client.py

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,24 @@
33
import os
44
import shutil
55
import warnings
6-
from functools import wraps, cache
7-
from typing import Optional, Any
6+
from functools import cache, wraps
7+
from typing import Any, Callable, Optional
88
from urllib.parse import urlparse
99

1010
import dateutil.parser
1111
import requests
1212

1313
from marble_client.constants import CACHE_FNAME, NODE_REGISTRY_URL
14-
from marble_client.exceptions import UnknownNodeError, JupyterEnvironmentError
14+
from marble_client.exceptions import JupyterEnvironmentError, UnknownNodeError
1515
from marble_client.node import MarbleNode
1616

1717
__all__ = ["MarbleClient"]
1818

1919

20-
def check_jupyterlab(f):
20+
def check_jupyterlab(f: Callable) -> Callable:
2121
"""
22+
Raise an error if not running in a Jupyterlab instance.
23+
2224
Wraps the function f by first checking if the current script is running in a
2325
Marble Jupyterlab environment and raising a JupyterEnvironmentError if not.
2426
@@ -28,22 +30,27 @@ def check_jupyterlab(f):
2830
Note that this checks if either the BIRDHOUSE_HOST_URL or PAVICS_HOST_URL are present to support
2931
versions of birdhouse-deploy prior to 2.4.0.
3032
"""
33+
3134
@wraps(f)
32-
def wrapper(*args, **kwargs):
35+
def wrapper(*args, **kwargs) -> Any:
3336
birdhouse_host_var = ("PAVICS_HOST_URL", "BIRDHOUSE_HOST_URL")
3437
jupyterhub_env_vars = ("JUPYTERHUB_API_URL", "JUPYTERHUB_USER", "JUPYTERHUB_API_TOKEN")
3538
if any(os.getenv(var) for var in birdhouse_host_var) and all(os.getenv(var) for var in jupyterhub_env_vars):
3639
return f(*args, **kwargs)
3740
raise JupyterEnvironmentError("Not in a Marble jupyterlab environment")
41+
3842
return wrapper
3943

4044

4145
class MarbleClient:
46+
"""Client object representing the information in the Marble registry."""
47+
4248
_registry_cache_key = "marble_client_python:cached_registry"
4349
_registry_cache_last_updated_key = "marble_client_python:last_updated"
4450

4551
def __init__(self, fallback: bool = True) -> None:
46-
"""Constructor method
52+
"""
53+
Initialize a MarbleClient instance.
4754
4855
:param fallback: If True, then fall back to a cached version of the registry
4956
if the cloud registry cannot be accessed, defaults to True
@@ -64,6 +71,7 @@ def __init__(self, fallback: bool = True) -> None:
6471

6572
@property
6673
def nodes(self) -> dict[str, MarbleNode]:
74+
"""Return nodes in the current registry."""
6775
return self._nodes
6876

6977
@property
@@ -87,15 +95,18 @@ def this_node(self) -> MarbleNode:
8795
def this_session(self, session: Optional[requests.Session] = None) -> requests.Session:
8896
"""
8997
Add the login session cookies of the user who is currently logged in to the session object.
98+
9099
If a session object is not passed as an argument to this function, create a new session
91100
object as well.
92101
93102
Note that this function only works in a Marble Jupyterlab environment.
94103
"""
95104
if session is None:
96105
session = requests.Session()
97-
r = requests.get(f"{os.getenv('JUPYTERHUB_API_URL')}/users/{os.getenv('JUPYTERHUB_USER')}",
98-
headers={"Authorization": f"token {os.getenv('JUPYTERHUB_API_TOKEN')}"})
106+
r = requests.get(
107+
f"{os.getenv('JUPYTERHUB_API_URL')}/users/{os.getenv('JUPYTERHUB_USER')}",
108+
headers={"Authorization": f"token {os.getenv('JUPYTERHUB_API_TOKEN')}"},
109+
)
99110
try:
100111
r.raise_for_status()
101112
except requests.HTTPError as err:
@@ -105,17 +116,20 @@ def this_session(self, session: Optional[requests.Session] = None) -> requests.S
105116
return session
106117

107118
@property
108-
def registry_uri(self):
119+
def registry_uri(self) -> str:
120+
"""Return the URL of the currently used Marble registry."""
109121
return self._registry_uri
110122

111123
def __getitem__(self, node: str) -> MarbleNode:
124+
"""Return the node with the given name."""
112125
try:
113126
return self.nodes[node]
114127
except KeyError as err:
115128
raise UnknownNodeError(f"No node named '{node}' in the Marble network.") from err
116129

117130
def __contains__(self, node: str) -> bool:
118-
"""Check if a node is available
131+
"""
132+
Check if a node is available.
119133
120134
:param node: ID of the Marble node
121135
:type node: str
@@ -175,8 +189,10 @@ def _save_registry_as_cache(self, registry: dict[str, Any]) -> None:
175189

176190
try:
177191
with open(CACHE_FNAME, "w") as f:
178-
data = {self._registry_cache_key: registry,
179-
self._registry_cache_last_updated_key: datetime.datetime.now(tz=datetime.timezone.utc).isoformat()}
192+
data = {
193+
self._registry_cache_key: registry,
194+
self._registry_cache_last_updated_key: datetime.datetime.now(tz=datetime.timezone.utc).isoformat(),
195+
}
180196
json.dump(data, f)
181197
except OSError:
182198
# If the cache file cannot be written, then restore from backup files

marble_client/exceptions.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
class MarbleBaseError(Exception):
2-
pass
2+
"""Base Error for all exceptions for this package."""
33

44

55
class ServiceNotAvailableError(MarbleBaseError):
6-
pass
6+
"""Indicates that a given service is not available."""
77

88

99
class UnknownNodeError(MarbleBaseError):
10-
pass
10+
"""Indicates that the given node cannot be found."""
1111

1212

1313
class JupyterEnvironmentError(MarbleBaseError):
14-
pass
14+
"""Indicates that there is an issue detecting features only available in Jupyterlab."""

marble_client/node.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313

1414
class MarbleNode:
15+
"""A node in the Marble network."""
16+
1517
def __init__(self, nodeid: str, jsondata: dict[str]) -> None:
1618
self._nodedata = jsondata
1719
self._id = nodeid
@@ -34,6 +36,7 @@ def __init__(self, nodeid: str, jsondata: dict[str]) -> None:
3436
self._services[s.name] = s
3537

3638
def is_online(self) -> bool:
39+
"""Return True iff the node is currently online."""
3740
try:
3841
registry = requests.get(self.url)
3942
registry.raise_for_status()
@@ -43,68 +46,89 @@ def is_online(self) -> bool:
4346

4447
@property
4548
def id(self) -> str:
49+
"""Return the unique id for this node in the Marble network."""
4650
return self._id
4751

4852
@property
4953
def name(self) -> str:
54+
"""
55+
Return the name of the node.
56+
57+
Note that this is not guarenteed to be unique (like the id) but represents
58+
how the node is often referred to in other documentation.
59+
"""
5060
return self._name
5161

5262
@property
5363
def description(self) -> str:
64+
"""Return a description of the node."""
5465
return self._nodedata["description"]
5566

5667
@property
5768
def url(self) -> Optional[str]:
69+
"""Return the root URL of the node."""
5870
return self._links_service
5971

6072
@property
6173
def collection_url(self) -> Optional[str]:
74+
"""Return a URL to the node's services endpoint."""
6275
warnings.warn("collection_url has been renamed to services_url", DeprecationWarning, 2)
6376
return self._links_collection
6477

6578
@property
6679
def services_url(self) -> Optional[str]:
80+
"""Return a URL to the node's services endpoint."""
6781
return self._links_collection
6882

6983
@property
7084
def version_url(self) -> Optional[str]:
85+
"""Return a URL to the node's version endpoint."""
7186
return self._links_version
7287

7388
@property
7489
def date_added(self) -> datetime:
90+
"""Return datetime representing when the node was added to the Marble network."""
7591
return dateutil.parser.isoparse(self._nodedata["date_added"])
7692

7793
@property
7894
def affiliation(self) -> str:
95+
"""Return affiliation information for the node."""
7996
return self._nodedata["affiliation"]
8097

8198
@property
8299
def location(self) -> dict[str, float]:
100+
"""Return the geographical location of the node."""
83101
return self._nodedata["location"]
84102

85103
@property
86104
def contact(self) -> str:
105+
"""Return contact information for the node."""
87106
return self._nodedata["contact"]
88107

89108
@property
90109
def last_updated(self) -> datetime:
110+
"""Return datetime representing the last time the node's metadata was updated."""
91111
return dateutil.parser.isoparse(self._nodedata["last_updated"])
92112

93113
@property
94114
def marble_version(self) -> str:
115+
"""Return node version."""
95116
warnings.warn("marble_version has been renamed to version", DeprecationWarning, 2)
96117
return self._nodedata["version"]
97118

98119
@property
99120
def version(self) -> str:
121+
"""Return node version."""
100122
return self._nodedata["version"]
101123

102124
@property
103125
def services(self) -> list[str]:
126+
"""Return node services."""
104127
return list(self._services)
105128

106129
@property
107130
def links(self) -> list[dict[str, str]]:
131+
"""Return node links."""
108132
return self._nodedata["links"]
109133

110134
def __getitem__(self, service: str) -> MarbleService:
@@ -122,7 +146,8 @@ def __getitem__(self, service: str) -> MarbleService:
122146
raise ServiceNotAvailableError(f"A service named '{service}' is not available on this node.") from e
123147

124148
def __contains__(self, service: str) -> bool:
125-
"""Check if a service is available at a node
149+
"""
150+
Check if a service is available at a node.
126151
127152
:param service: Name of the Marble service
128153
:type service: str
@@ -132,4 +157,5 @@ def __contains__(self, service: str) -> bool:
132157
return service in self._services
133158

134159
def __repr__(self) -> str:
160+
"""Return a repr containing id and name."""
135161
return f"<{self.__class__.__name__}(id: '{self.id}', name: '{self.name}')>"

0 commit comments

Comments
 (0)