Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 10 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,16 @@ on:
- main

jobs:
# Do not lint for now, as the code is not yet compliant
#lint:
# name: Lint
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: astral-sh/setup-uv@v3
# - name: Ruff lint
# run: uv run ruff check .
# - name: Ruff format
# run: uv run ruff format --diff .
# # This isn't a general Python lint, this style is just used in this repository
# - name: Prettier format
# run: npx prettier --prose-wrap always --check "**/*.md"

lint:
name: Run linting and format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uv python install python3.13
- run: uv run ruff check src/obelisk/
- run: uv run ruff format --check src/obelisk/
- run: uv run mypy src/obelisk/
test:
name: Run tests
strategy:
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ In case of major restructuring, it may be needed to clean up the contents of `do
followed by re-running the build.
Manually triggering sphinx-apidoc is unnecessary.

### Hooks

We use some simple Git hooks to avoid fighting the CI too often.
These are stored in the `hooks/` directory, and can be enabled by setting `git config core.hooksPath hooks/`.

## Credits

Base implementation originally by Pieter Moens <Pieter.Moens@ugent.be>,
Expand Down
14 changes: 14 additions & 0 deletions hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.
#
# To enable this hook, rename this file to "pre-commit".

# Redirect output to stderr.
exec 1>&2

uv run ruff format --check src/obelisk/
uv run mypy src/obelisk/
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ dependencies = [
"pydantic>=2.10.6",
"pytest>=8.3.5",
"pytest-asyncio>=0.25.3",
"mypy>=1.18.2",
]
authors = [
{ name="Stef Pletinck", email="Stef.Pletinck@ugent.be"},
Expand Down
Empty file removed src/__init__.py
Empty file.
3 changes: 2 additions & 1 deletion src/obelisk/asynchronous/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
Relevant entrance points are :class:`client.Obelisk`.
It can be imported from the :mod:`.client` module, or directly from this one.
"""
__all__= ['Obelisk', 'core']

__all__ = ["Obelisk", "core"]
from .client import Obelisk
from . import core
87 changes: 49 additions & 38 deletions src/obelisk/asynchronous/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
import httpx

from obelisk.exceptions import AuthenticationError
from obelisk.strategies.retry import RetryStrategy, \
NoRetryStrategy
from obelisk.strategies.retry import RetryStrategy, NoRetryStrategy
from obelisk.types import ObeliskKind


Expand All @@ -32,27 +31,33 @@ class BaseClient:

log: logging.Logger

def __init__(self, client: str, secret: str,
retry_strategy: RetryStrategy = NoRetryStrategy(),
kind: ObeliskKind = ObeliskKind.CLASSIC) -> None:
def __init__(
self,
client: str,
secret: str,
retry_strategy: RetryStrategy = NoRetryStrategy(),
kind: ObeliskKind = ObeliskKind.CLASSIC,
) -> None:
self._client = client
self._secret = secret
self.retry_strategy = retry_strategy
self.kind = kind

self.log = logging.getLogger('obelisk')
self.log = logging.getLogger("obelisk")

async def _get_token(self):
auth_string = str(base64.b64encode(
f'{self._client}:{self._secret}'.encode('utf-8')), 'utf-8')
auth_string = str(
base64.b64encode(f"{self._client}:{self._secret}".encode("utf-8")), "utf-8"
)
headers = {
'Authorization': f'Basic {auth_string}',
'Content-Type': ('application/json'
if self.kind.use_json_auth else 'application/x-www-form-urlencoded')
}
payload = {
'grant_type': 'client_credentials'
"Authorization": f"Basic {auth_string}",
"Content-Type": (
"application/json"
if self.kind.use_json_auth
else "application/x-www-form-urlencoded"
),
}
payload = {"grant_type": "client_credentials"}

async with httpx.AsyncClient() as client:
response = None
Expand All @@ -64,7 +69,8 @@ async def _get_token(self):
self.kind.token_url,
json=payload if self.kind.use_json_auth else None,
data=payload if not self.kind.use_json_auth else None,
headers=headers)
headers=headers,
)

response = request.json()
except Exception as e:
Expand All @@ -76,29 +82,33 @@ async def _get_token(self):
raise last_error

if request.status_code != 200:
if 'error' in response:
if "error" in response:
self.log.warning(f"Could not authenticate, {response['error']}")
raise AuthenticationError

self._token = response['access_token']
self._token_expires = (datetime.now()
+ timedelta(seconds=response['expires_in']))
self._token = response["access_token"]
self._token_expires = datetime.now() + timedelta(
seconds=response["expires_in"]
)

async def _verify_token(self):
if (self._token is None
or self._token_expires < (datetime.now() - self.grace_period)):
if self._token is None or self._token_expires < (
datetime.now() - self.grace_period
):
retry = self.retry_strategy.make()
first = True
while first or await retry.should_retry():
first = False
try:
await self._get_token()
return
except:
except: # noqa: E722
self.log.info("excepted, Retrying token fetch")
continue

async def http_post(self, url: str, data: Any = None,
params: Optional[dict] = None) -> httpx.Response:
async def http_post(
self, url: str, data: Any = None, params: Optional[dict] = None
) -> httpx.Response:
"""
Send an HTTP POST request to Obelisk,
with proper auth.
Expand All @@ -113,8 +123,8 @@ async def http_post(self, url: str, data: Any = None,
await self._verify_token()

headers = {
'Authorization': f'Bearer {self._token}',
'Content-Type': 'application/json'
"Authorization": f"Bearer {self._token}",
"Content-Type": "application/json",
}
if params is None:
params = {}
Expand All @@ -127,11 +137,12 @@ async def http_post(self, url: str, data: Any = None,
self.log.debug(f"Retrying, last response: {response.status_code}")

try:
response = await client.post(url,
json=data,
params={k: v for k, v in params.items() if
v is not None},
headers=headers)
response = await client.post(
url,
json=data,
params={k: v for k, v in params.items() if v is not None},
headers=headers,
)

if response.status_code // 100 == 2:
return response
Expand All @@ -144,7 +155,6 @@ async def http_post(self, url: str, data: Any = None,
raise last_error
return response


async def http_get(self, url: str, params: Optional[dict] = None) -> httpx.Response:
"""
Send an HTTP GET request to Obelisk,
Expand All @@ -160,8 +170,8 @@ async def http_get(self, url: str, params: Optional[dict] = None) -> httpx.Respo
await self._verify_token()

headers = {
'Authorization': f'Bearer {self._token}',
'Content-Type': 'application/json'
"Authorization": f"Bearer {self._token}",
"Content-Type": "application/json",
}
if params is None:
params = {}
Expand All @@ -174,10 +184,11 @@ async def http_get(self, url: str, params: Optional[dict] = None) -> httpx.Respo
self.log.debug(f"Retrying, last response: {response.status_code}")

try:
response = await client.get(url,
params={k: v for k, v in params.items() if
v is not None},
headers=headers)
response = await client.get(
url,
params={k: v for k, v in params.items() if v is not None},
headers=headers,
)

if response.status_code // 100 == 2:
return response
Expand Down
6 changes: 3 additions & 3 deletions src/obelisk/asynchronous/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from datetime import datetime, timedelta
from math import floor
from typing import AsyncGenerator, Generator, List, Literal, Optional
from typing import AsyncGenerator, List, Literal, Optional

import httpx
from pydantic import ValidationError
Expand Down Expand Up @@ -90,7 +90,8 @@ async def fetch_single_chunk(
"limitBy": limit_by,
}
response = await self.http_post(
self.kind.query_url, data={k: v for k, v in payload.items() if v is not None}
self.kind.query_url,
data={k: v for k, v in payload.items() if v is not None},
)
if response.status_code != 200:
self.log.warning(f"Unexpected status code: {response.status_code}")
Expand Down Expand Up @@ -188,7 +189,6 @@ async def query(

return result_set


async def query_time_chunked(
self,
datasets: List[str],
Expand Down
Loading