Skip to content

Commit

Permalink
chore(github): please mypy
Browse files Browse the repository at this point in the history
I'm dead.
  • Loading branch information
vit-zikmund committed Feb 27, 2024
1 parent 4d297fa commit 727351c
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 28 deletions.
66 changes: 42 additions & 24 deletions giftless/auth/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@
from contextlib import AbstractContextManager
from operator import attrgetter, itemgetter
from threading import Condition, Lock, RLock
from typing import Any
from typing import Any, cast, overload

import cachetools.keys
import flask
import marshmallow as ma
import marshmallow.validate
import requests

from giftless.auth import Identity, Unauthorized
from giftless.auth.identity import Permission
from giftless.auth import Unauthorized
from giftless.auth.identity import Identity, Permission

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -45,12 +45,26 @@ def _ensure_lock(
return existing_lock


@overload
def single_call_method(_method: Callable[..., Any]) -> Callable[..., Any]:
...


@overload
def single_call_method(
*,
key: Callable[..., Any] = cachetools.keys.methodkey,
lock: Callable[[Any], AbstractContextManager] | None = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
...


def single_call_method(
_method: Callable[[...], Any] | None = None,
_method: Callable[..., Any] | None = None,
*,
key: Callable = cachetools.keys.methodkey,
key: Callable[..., Any] = cachetools.keys.methodkey,
lock: Callable[[Any], AbstractContextManager] | None = None,
) -> Callable[[...], Any]:
) -> Callable[..., Any]:
"""Thread-safe decorator limiting concurrency of an idempotent method call.
When multiple threads concurrently call the decorated method with the same
arguments (governed by the 'key' callable argument), only the first one
Expand All @@ -68,9 +82,9 @@ def single_call_method(
"""
lock = _ensure_lock(lock)

def decorator(method: Callable) -> Callable:
def decorator(method: Callable[..., Any]) -> Callable[..., Any]:
# tracking concurrent calls per method arguments
concurrent_calls = {}
concurrent_calls: dict[Any, SingleCallContext] = {}

@functools.wraps(method)
def wrapper(self: Any, *args: tuple, **kwargs: dict) -> Any:
Expand Down Expand Up @@ -123,13 +137,13 @@ def wrapper(self: Any, *args: tuple, **kwargs: dict) -> Any:

def cachedmethod_threadsafe(
cache: Callable[[Any], MutableMapping],
key: Callable = cachetools.keys.methodkey,
key: Callable[..., Any] = cachetools.keys.methodkey,
lock: Callable[[Any], AbstractContextManager] | None = None,
) -> Callable:
) -> Callable[..., Any]:
"""Threadsafe variant of cachetools.cachedmethod."""
lock = _ensure_lock(lock)

def decorator(method: Callable) -> Callable:
def decorator(method: Callable[..., Any]) -> Callable[..., Any]:
@cachetools.cachedmethod(cache=cache, key=key, lock=lock)
@single_call_method(key=key, lock=lock)
@functools.wraps(method)
Expand Down Expand Up @@ -216,7 +230,7 @@ def make_object(

@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> "Config":
return cls.Schema().load(data, unknown=ma.RAISE)
return cast(Config, cls.Schema().load(data, unknown=ma.RAISE))


# CORE AUTH
Expand Down Expand Up @@ -288,19 +302,21 @@ def is_authorized(
oid: str | None = None,
) -> bool:
permissions = self.permissions(organization, repo)
return permissions and permission in permissions
return permission in permissions if permissions else False

def cache_ttl(self, permissions: set[Permission]) -> float:
"""Return default cache TTL [seconds] for a certain permission set."""
return self._auth_cache.ttu(None, permissions, 0.0)

@staticmethod
def cache_key(data: dict) -> tuple:
def cache_key(data: Mapping[str, Any]) -> tuple:
"""Return caching key from significant fields."""
return cachetools.keys.hashkey(*itemgetter("login", "id")(data))

@classmethod
def from_dict(cls, data: dict, cc: CacheConfig) -> "GithubIdentity":
def from_dict(
cls, data: Mapping[str, Any], cc: CacheConfig
) -> "GithubIdentity":
return cls(*itemgetter("login", "id", "name", "email")(data), cc=cc)


Expand Down Expand Up @@ -344,26 +360,28 @@ def __init__(self, cfg: Config) -> None:
if cfg.api_version:
self._api_headers["X-GitHub-Api-Version"] = cfg.api_version
# user identities per raw user data (keeping them authorized)
self._user_cache = cachetools.LRUCache(maxsize=cfg.cache.user_max_size)
self._user_cache: MutableMapping[
Any, GithubIdentity
] = cachetools.LRUCache(maxsize=cfg.cache.user_max_size)
# user identities per token (shortcut to the cached entries above)
self._token_cache = cachetools.LRUCache(
maxsize=cfg.cache.token_max_size
)
self._token_cache: MutableMapping[
Any, GithubIdentity
] = cachetools.LRUCache(maxsize=cfg.cache.token_max_size)
self._cache_config = cfg.cache

def _api_get(self, uri: str, ctx: CallContext) -> dict:
def _api_get(self, uri: str, ctx: CallContext) -> Mapping[str, Any]:
response = ctx.session.get(
f"{self._api_url}{uri}",
headers={"Authorization": f"Bearer {ctx.token}"},
)
response.raise_for_status()
return response.json()
return cast(Mapping[str, Any], response.json())

@cachedmethod_threadsafe(
attrgetter("_user_cache"),
lambda self, data: GithubIdentity.cache_key(data),
)
def _get_user_cached(self, data: dict) -> GithubIdentity:
def _get_user_cached(self, data: Mapping[str, Any]) -> GithubIdentity:
"""Return internal GitHub user identity from raw GitHub user data
[cached per login & id].
"""
Expand All @@ -385,7 +403,7 @@ def _authenticate(self, ctx: CallContext) -> GithubIdentity:
raise Unauthorized(msg) from None

# different tokens can bear the same identity
return self._get_user_cached(user_data)
return cast(GithubIdentity, self._get_user_cached(user_data))

@staticmethod
def _perm_list(permissions: set[Permission]) -> str:
Expand Down Expand Up @@ -448,7 +466,7 @@ def __call__(self, request: flask.Request) -> Identity | None:
with requests.Session() as session:
session.headers.update(self._api_headers)
ctx = self.CallContext(request, session)
user = self._authenticate(ctx)
user: GithubIdentity = self._authenticate(ctx)
_logger.info(f"Authenticated the user as {user}")
self._authorize(ctx, user)
return user
Expand Down
1 change: 1 addition & 0 deletions requirements/dev.in
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ types-pytz
types-jwt
types-python-dateutil
types-PyYAML
types-cachetools

boto3-stubs
botocore-stubs
Expand Down
24 changes: 20 additions & 4 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile with Python 3.12
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --no-emit-index-url --output-file=requirements/dev.txt requirements/dev.in
Expand Down Expand Up @@ -44,15 +44,17 @@ colorama==0.4.6
commonmark==0.9.1
# via recommonmark
coverage[toml]==7.4.0
# via
# coverage
# pytest-cov
# via pytest-cov
distlib==0.3.8
# via virtualenv
docutils==0.20.1
# via
# recommonmark
# sphinx
exceptiongroup==1.2.0
# via
# -c requirements/main.txt
# pytest
filelock==3.13.1
# via
# pytest-mypy
Expand Down Expand Up @@ -199,10 +201,23 @@ sphinxcontrib-qthelp==1.0.7
# via sphinx
sphinxcontrib-serializinghtml==1.1.10
# via sphinx
tomli==2.0.1
# via
# build
# coverage
# mypy
# pip-tools
# pyproject-api
# pyproject-hooks
# pytest
# pytest-env
# tox
tox==4.12.0
# via -r requirements/dev.in
types-awscrt==0.20.0
# via botocore-stubs
types-cachetools==5.3.0.7
# via -r requirements/dev.in
types-cryptography==3.3.23.2
# via types-jwt
types-jwt==0.1.3
Expand All @@ -220,6 +235,7 @@ types-s3transfer==0.10.0
typing-extensions==4.9.0
# via
# -c requirements/main.txt
# boto3-stubs
# mypy
urllib3==2.0.7
# via
Expand Down

0 comments on commit 727351c

Please sign in to comment.