Skip to content
Closed
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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Ran Black on the entire code base 2025-04-14
118d0de77071253933a2c9ea16cc4f6c05dbf427
52 changes: 33 additions & 19 deletions ietf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,49 @@
# -*- coding: utf-8 -*-


from . import checks # pyflakes:ignore
from . import checks # pyflakes:ignore

# Version must stay in single quotes for automatic CI replace
# Don't add patch number here:
__version__ = '1.0.0-dev'
__version__ = "1.0.0-dev"

# Release hash must stay in single quotes for automatic CI replace
__release_hash__ = ''
__release_hash__ = ""

# Release branch must stay in single quotes for automatic CI replace
__release_branch__ = ''
__release_branch__ = ""

# set this to ".p1", ".p2", etc. after patching
__patch__ = ""
__patch__ = ""

if __version__ == '1.0.0-dev' and __release_hash__ == '' and __release_branch__ == '':
if __version__ == "1.0.0-dev" and __release_hash__ == "" and __release_branch__ == "":
import subprocess
branch = subprocess.run(
["/usr/bin/git", "branch", "--show-current"],
capture_output=True,
).stdout.decode().strip()
git_hash = subprocess.run(
["/usr/bin/git", "rev-parse", "head"],
capture_output=True,
).stdout.decode().strip()
rev = subprocess.run(
["/usr/bin/git", "describe", "--tags", git_hash],
capture_output=True,
).stdout.decode().strip().split('-', 1)[0]

branch = (
subprocess.run(
["/usr/bin/git", "branch", "--show-current"],
capture_output=True,
)
.stdout.decode()
.strip()
)
git_hash = (
subprocess.run(
["/usr/bin/git", "rev-parse", "head"],
capture_output=True,
)
.stdout.decode()
.strip()
)
rev = (
subprocess.run(
["/usr/bin/git", "describe", "--tags", git_hash],
capture_output=True,
)
.stdout.decode()
.strip()
.split("-", 1)[0]
)
__version__ = f"{rev}-dev"
__release_branch__ = branch
__release_hash__ = git_hash
Expand All @@ -40,4 +54,4 @@
# Django starts so that shared_task will use this app.
from .celeryapp import app as celery_app

__all__ = ('celery_app',)
__all__ = ("celery_app",)
2 changes: 1 addition & 1 deletion ietf/admin/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@


class AdminConfig(admin_apps.AdminConfig):
default_site = "ietf.admin.sites.AdminSite"
default_site = "ietf.admin.sites.AdminSite"
6 changes: 4 additions & 2 deletions ietf/admin/sites.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

class AdminSite(_AdminSite):
site_title = "Datatracker admin"

@staticmethod
def site_header():
if settings.SERVER_MODE == "production":
return "Datatracker administration"
else:
return mark_safe('Datatracker administration <span class="text-danger">&delta;</span>')
return mark_safe(
'Datatracker administration <span class="text-danger">&delta;</span>'
)
89 changes: 68 additions & 21 deletions ietf/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from django.utils.module_loading import autodiscover_modules


import debug # pyflakes:ignore
import debug # pyflakes:ignore

import tastypie.resources
import tastypie.serializers
Expand All @@ -25,17 +25,19 @@

OMITTED_APPS_APIS = ["ietf.status"]


def populate_api_list():
_module_dict = globals()
for app_config in django_apps.get_app_configs():
if '.' in app_config.name and app_config.name not in OMITTED_APPS_APIS:
_root, _name = app_config.name.split('.', 1)
if _root == 'ietf':
if not '.' in _name:
if "." in app_config.name and app_config.name not in OMITTED_APPS_APIS:
_root, _name = app_config.name.split(".", 1)
if _root == "ietf":
if not "." in _name:
_api = Api(api_name=_name)
_module_dict[_name] = _api
_api_list.append((_name, _api))


def autodiscover():
"""
Auto-discover INSTALLED_APPS resources.py modules and fail silently when
Expand All @@ -52,17 +54,25 @@ def generate_cache_key(self, *args, **kwargs):

This is based off the current api_name/resource_name/args/kwargs.
"""
#smooshed = ["%s=%s" % (key, value) for key, value in kwargs.items()]
# smooshed = ["%s=%s" % (key, value) for key, value in kwargs.items()]
smooshed = urlencode(kwargs)

# Use a list plus a ``.join()`` because it's faster than concatenation.
return "%s:%s:%s:%s" % (self._meta.api_name, self._meta.resource_name, ':'.join(args), smooshed)
return "%s:%s:%s:%s" % (
self._meta.api_name,
self._meta.resource_name,
":".join(args),
smooshed,
)


TIMEDELTA_REGEX = re.compile(
r"^(?P<days>\d+d)?\s?(?P<hours>\d+h)?\s?(?P<minutes>\d+m)?\s?(?P<seconds>\d+s?)$"
)

TIMEDELTA_REGEX = re.compile(r'^(?P<days>\d+d)?\s?(?P<hours>\d+h)?\s?(?P<minutes>\d+m)?\s?(?P<seconds>\d+s?)$')

class TimedeltaField(ApiField):
dehydrated_type = 'timedelta'
dehydrated_type = "timedelta"
help_text = "A timedelta field, with duration expressed in seconds. Ex: 132"

def convert(self, value):
Expand All @@ -74,41 +84,61 @@ def convert(self, value):

if match:
data = match.groupdict()
return datetime.timedelta(int(data['days']), int(data['hours']), int(data['minutes']), int(data['seconds']))
return datetime.timedelta(
int(data["days"]),
int(data["hours"]),
int(data["minutes"]),
int(data["seconds"]),
)
else:
raise ApiFieldError("Timedelta provided to '%s' field doesn't appear to be a valid timedelta string: '%s'" % (self.instance_name, value))
raise ApiFieldError(
"Timedelta provided to '%s' field doesn't appear to be a valid timedelta string: '%s'"
% (self.instance_name, value)
)

return value

def hydrate(self, bundle):
value = super(TimedeltaField, self).hydrate(bundle)

if value and not hasattr(value, 'seconds'):
if value and not hasattr(value, "seconds"):
if isinstance(value, str):
try:
match = TIMEDELTA_REGEX.search(value)

if match:
data = match.groupdict()
value = datetime.timedelta(int(data['days']), int(data['hours']), int(data['minutes']), int(data['seconds']))
value = datetime.timedelta(
int(data["days"]),
int(data["hours"]),
int(data["minutes"]),
int(data["seconds"]),
)
else:
raise ValueError()
except (ValueError, TypeError):
raise ApiFieldError("Timedelta provided to '%s' field doesn't appear to be a valid datetime string: '%s'" % (self.instance_name, value))
raise ApiFieldError(
"Timedelta provided to '%s' field doesn't appear to be a valid datetime string: '%s'"
% (self.instance_name, value)
)

else:
raise ApiFieldError("Datetime provided to '%s' field must be a string: %s" % (self.instance_name, value))
raise ApiFieldError(
"Datetime provided to '%s' field must be a string: %s"
% (self.instance_name, value)
)

return value


class ToOneField(tastypie.fields.ToOneField):
"Subclass of tastypie.fields.ToOneField which adds caching in the dehydrate method."

def dehydrate(self, bundle, for_list=True):
foreign_obj = None
previous_obj = None
attrib = None

if callable(self.attribute):
previous_obj = bundle.obj
foreign_obj = self.attribute(bundle)
Expand All @@ -126,24 +156,41 @@ def dehydrate(self, bundle, for_list=True):
if not foreign_obj:
if not self.null:
if callable(self.attribute):
raise ApiFieldError("The related resource for resource %s could not be found." % (previous_obj))
raise ApiFieldError(
"The related resource for resource %s could not be found."
% (previous_obj)
)
else:
raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (previous_obj, attrib))
raise ApiFieldError(
"The model '%r' has an empty attribute '%s' and doesn't allow a null value."
% (previous_obj, attrib)
)
return None

fk_resource = self.get_related_resource(foreign_obj)

# Up to this point we've copied the code from tastypie 0.13.1. Now
# we add caching.
cache_key = fk_resource.generate_cache_key('related', pk=foreign_obj.pk, for_list=for_list, )
cache_key = fk_resource.generate_cache_key(
"related",
pk=foreign_obj.pk,
for_list=for_list,
)
dehydrated = fk_resource._meta.cache.get(cache_key)
if dehydrated is None:
fk_bundle = Bundle(obj=foreign_obj, request=bundle.request)
dehydrated = self.dehydrate_related(fk_bundle, fk_resource, for_list=for_list)
dehydrated = self.dehydrate_related(
fk_bundle, fk_resource, for_list=for_list
)
fk_resource._meta.cache.set(cache_key, dehydrated)
return dehydrated


class Serializer(tastypie.serializers.Serializer):
def format_datetime(self, data):
return data.astimezone(datetime.timezone.utc).replace(tzinfo=None).isoformat(timespec="seconds") + "Z"
return (
data.astimezone(datetime.timezone.utc)
.replace(tzinfo=None)
.isoformat(timespec="seconds")
+ "Z"
)
42 changes: 21 additions & 21 deletions ietf/api/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,30 @@
from typing import Any, List
import tastypie

community = ... # type: Any
dbtemplate = ... # type: Any
doc = ... # type: Any
group = ... # type: Any
iesg = ... # type: Any
ipr = ... # type: Any
liaisons = ... # type: Any
mailinglists = ... # type: Any
mailtrigger = ... # type: Any
meeting = ... # type: Any
message = ... # type: Any
name = ... # type: Any
nomcom = ... # type: Any
person = ... # type: Any
redirects = ... # type: Any
review = ... # type: Any
stats = ... # type: Any
submit = ... # type: Any
utils = ... # type: Any
community = ... # type: Any
dbtemplate = ... # type: Any
doc = ... # type: Any
group = ... # type: Any
iesg = ... # type: Any
ipr = ... # type: Any
liaisons = ... # type: Any
mailinglists = ... # type: Any
mailtrigger = ... # type: Any
meeting = ... # type: Any
message = ... # type: Any
name = ... # type: Any
nomcom = ... # type: Any
person = ... # type: Any
redirects = ... # type: Any
review = ... # type: Any
stats = ... # type: Any
submit = ... # type: Any
utils = ... # type: Any

_api_list = ... # type: List
_api_list = ... # type: List

class ModelResource(tastypie.resources.ModelResource): ...
class Serializer(): ...
class Serializer: ...
class ToOneField(tastypie.fields.ToOneField): ...
class TimedeltaField(tastypie.fields.ApiField): ...

Expand Down
6 changes: 3 additions & 3 deletions ietf/api/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ class ApiConfig(AppConfig):

def ready(self):
"""Hook to do init after the app registry is fully populated

Importing models or accessing the app registry is ok here, but do not
interact with the database. See
interact with the database. See
https://docs.djangoproject.com/en/4.2/ref/applications/#django.apps.AppConfig.ready
"""
# Populate our API list now that the app registry is set up
populate_api_list()

# Import drf-spectacular extensions
# Import drf-spectacular extensions
import ietf.api.schema # pyflakes: ignore
2 changes: 1 addition & 1 deletion ietf/api/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class ApiKeyAuthentication(authentication.BaseAuthentication):

def authenticate(self, request):
"""Extract the authentication token, if present

This does not validate the token, it just arranges for it to be available in request.auth.
It's up to a Permissions class to validate it for the appropriate endpoint.
"""
Expand Down
Loading