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
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,6 @@ jobs:
type-check:
name: Type check
runs-on: ubuntu-latest
# Advisory while the diagnostics already in the tree are worked through: the
# job reports them, it does not fail the pull request.
continue-on-error: true
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Expand All @@ -93,9 +90,12 @@ jobs:

- name: Install the package and the type checker
# ty resolves third-party imports out of the environment the package is
# installed in, so the extras have to be here too.
# installed in, so the extras have to be here too. docs is among them
# because ascenderkit/cli/sphinx.py imports docutils and autoprogram at
# module level. formatting is not: jq ships as a compiled extension with
# no stubs, so installing it resolves nothing.
run: |
pip install -e ".[websockets,crypto]"
pip install -e ".[websockets,crypto,docs]"
pip install ty==0.0.74

- name: Run the type checker
Expand Down
14 changes: 10 additions & 4 deletions ascenderkit/api/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Any

import requests
from requests.auth import AuthBase
Expand All @@ -18,9 +19,11 @@ class Token_Auth(AuthBase):
def __init__(self, token):
self.token = token

def __call__(self, request):
request.headers['Authorization'] = f'Bearer {self.token}'
return request
# Named `r` because AuthBase.__call__ names it that, and a caller holding
# the base type is entitled to pass it by that keyword.
def __call__(self, r):
r.headers['Authorization'] = f'Bearer {self.token}'
return r


def log_elapsed(r, *args, **kwargs): # requests hook to display API elapsed time
Expand Down Expand Up @@ -87,7 +90,10 @@ def request(self, relative_endpoint, method='get', json=None, data=None, query_p
use_endpoint = use_endpoint[1:]
url = '/'.join([self.server, use_endpoint])

kwargs = dict(verify=self.verify, params=query_parameters, json=json, data=data, hooks=dict(response=log_elapsed))
# Annotated because the values are heterogeneous: inference narrows the
# dict to the union of the literals below, and the headers added further
# down are not in it.
kwargs: dict[str, Any] = dict(verify=self.verify, params=query_parameters, json=json, data=data, hooks=dict(response=log_elapsed))

if headers is not None:
kwargs['headers'] = headers
Expand Down
2 changes: 2 additions & 0 deletions ascenderkit/api/mixins/has_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ def _filter_ds_from_payload(obj, *a, **kw):
json.dumps = filter_ds_from_payload(json.dumps)

try:
# Not a dependency: it is patched only when something else in the
# environment has brought it in. See [[tool.ty.overrides]] in pyproject.toml.
import simplejson # noqa

simplejson.dumps = filter_ds_from_payload(simplejson.dumps)
Expand Down
4 changes: 2 additions & 2 deletions ascenderkit/api/pages/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ def wait_until_status(self, status, interval=5, timeout=30, **kwargs):
adjusted_timeout = timeout - seconds_since_date_string(self.created)
return super().wait_until_status(status, interval, adjusted_timeout, **kwargs)

def wait_until_completed(self, interval=5, timeout=240):
def wait_until_completed(self, interval=5, timeout=240, **kwargs):
"""Notifications need a longer timeout, since the backend often has
to wait for the request (sending the notification) to timeout itself
"""
adjusted_timeout = timeout - seconds_since_date_string(self.created)
return super().wait_until_completed(interval, adjusted_timeout)
return super().wait_until_completed(interval, adjusted_timeout, **kwargs)


page.register_page(resources.notification, Notification)
Expand Down
10 changes: 8 additions & 2 deletions ascenderkit/api/pages/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,10 @@ class PageList:
# Page.__init__, next and previous come from the response body through
# Page.__getattr__, and get is Page's own. Annotations rather than
# assignments, so nothing is created at runtime.
#
# self.__class__ is a Page subclass for the same reason. The two methods
# below hold it in a local first, because the mixin does not inherit Page
# and the only constructor visible behind the class itself is object's.
json: dict
connection: Any
r: Any
Expand Down Expand Up @@ -379,12 +383,14 @@ def results(self):

def go_to_next(self):
if self.next:
next_page = self.__class__(self.connection, endpoint=self.next)
page_class: Any = self.__class__
next_page = page_class(self.connection, endpoint=self.next)
return next_page.get()

def go_to_previous(self):
if self.previous:
prev_page = self.__class__(self.connection, endpoint=self.previous)
page_class: Any = self.__class__
prev_page = page_class(self.connection, endpoint=self.previous)
return prev_page.get()

def create(self, *a, **kw):
Expand Down
4 changes: 2 additions & 2 deletions ascenderkit/cli/custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,10 +257,10 @@ class AdHocCommandRelaunch(HasRelaunch, CustomAction):
class WorkflowJobRelaunch(HasRelaunch, CustomAction):
resource = 'workflow_jobs'

def add_arguments(self, parser, resource_options_parser):
def add_arguments(self, parser, resource_options_parser, with_pk=True):
# The endpoint takes an empty serializer, so OPTIONS advertises no
# fields and nothing below would be generated from it.
super().add_arguments(parser, resource_options_parser)
super().add_arguments(parser, resource_options_parser, with_pk=with_pk)
parser.choices[self.action].add_argument(
'--nodes',
choices=['all', 'failed'],
Expand Down
2 changes: 2 additions & 0 deletions ascenderkit/cli/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ def format_response(response, fmt='json', filter='.', changed=False):

def format_jq(output, fmt):
try:
# Ships as a compiled extension with no stubs, so it does not resolve to
# a type checker even when installed. See pyproject.toml.
import jq
except ImportError:
if fmt == '.':
Expand Down
7 changes: 4 additions & 3 deletions ascenderkit/cli/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ class CustomCommand(metaclass=CustomRegistryMeta):

help_text = ''

@property
def name(self):
raise NotImplementedError()
# Set as a plain class attribute by every subclass, so it is declared here
# rather than raised from a property that nothing ever reaches: the registry
# reads it off the class, where a property is the property itself.
name: str

def handle(self, client, parser):
"""To be implemented by subclasses.
Expand Down
6 changes: 6 additions & 0 deletions ascenderkit/cli/sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

from .client import CLI
from .resource import is_control_resource, CustomCommand
from .utils import HelpfulArgumentParser

# Bound by __getattr__ below on the first lookup, and by nothing else. An
# annotation rather than an assignment, so the name stays out of the module
# namespace and PEP 562 still routes that first lookup here.
parser: HelpfulArgumentParser


class CustomAutoprogramDirective(AutoprogramDirective):
Expand Down
6 changes: 6 additions & 0 deletions ascenderkit/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@


class CustomRegistryMeta(type):
# Every registered class carries one, either as a plain attribute or as a
# property on the metaclass that derives it. Declared here because the
# registry below reaches those classes through __subclasses__(), which
# describes them as this class rather than as themselves.
name: str

@property
def registry(cls):
return dict((command.name, command) for command in cls.__subclasses__())
Expand Down
8 changes: 8 additions & 0 deletions ascenderkit/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
from typing import Any


class Common(Exception):
# The decoded response body, which the platform renders as a dict for most
# errors, and as a string for the ones it does not serialize. Callers read
# it both ways, so it is not narrowed to either.
msg: Any

def __init__(self, status_string='', message=''):
if isinstance(status_string, Exception):
self.status_string = ''
Expand Down
2 changes: 2 additions & 0 deletions ascenderkit/scripts/basic_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ def load_interactive():
return main()

try:
# Not a dependency: the session falls back to code.interact without it.
# See [[tool.ty.overrides]] in pyproject.toml.
from IPython import start_ipython

basic_session_path = os.path.abspath(__file__)
Expand Down
18 changes: 18 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,24 @@ select = [
"W291", "W605",
]

[[tool.ty.overrides]]
# Three optional imports the package does not declare: simplejson is patched
# only when something else in the environment has brought it in, IPython decides
# which interactive session ascender-shell drops into, and jq backs `-f jq`.
# Whether each one resolves depends on the environment the checker runs in, so
# the rule is turned off for the files holding them. A `ty: ignore` on the line
# would not do: it becomes an unused suppression, and a warning, the moment one
# of the three is installed. jq is the permanent case of the two, since it ships
# as a compiled extension with no stubs and so resolves either way.
include = [
"ascenderkit/api/mixins/has_create.py",
"ascenderkit/cli/format.py",
"ascenderkit/scripts/basic_session.py",
]

[tool.ty.overrides.rules]
unresolved-import = "ignore"

[tool.pytest.ini_options]
# Moved here from a [pytest] section in tox.ini. pytest reads both, and this is
# the file the rest of the tooling configuration already lives in.
Expand Down
11 changes: 6 additions & 5 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,14 @@ commands =
sphinx-build -b html -W --keep-going ascenderkit/cli/docs/source {envtmpdir}/html

[testenv:typecheck]
# Advisory: the type checker runs over ascenderkit and reports what it finds
# without failing the merge, so the diagnostics already in the tree can be
# worked through a module at a time. It resolves third-party imports out of the
# environment the package is installed in; set TYPECHECK_PYTHON to point it at
# one. Pinned like the other linters so results do not drift.
# The type checker runs over ascenderkit and has to come back clean. It resolves
# third-party imports out of the environment the package is installed in; set
# TYPECHECK_PYTHON to point it at one. That is also why docs is installed here,
# since ascenderkit/cli/sphinx.py imports docutils and autoprogram at module
# level. Pinned like the other linters so results do not drift.
deps =
{[testenv]deps}
.[docs]
ty==0.0.74
passenv =
TYPECHECK_PYTHON
Expand Down