irongit

Enhanced time synchronization for drf views.

set up examples

huncholanehuncholaneauthored
parent 7ffd8b5commit 900bdd27c02a35920513ce7924b69e1a90eac52fBrowse files

24 files changed, +355 -44

+2-27.gitignore
@@ -1,29 +1,4 @@
1-# Bytecode
2-*.py[cod]
3-__pycache__/
4-
5-# Environments
6-.env
7-.venv/
8-venv/
9-
10-# Editor
11-.vscode/
12-.idea/
13-*.swp
14-
15-# Build artifacts
16-*.egg-info/
17-dist/
181 build/
19-
20-# Django
21-db.sqlite3
22-*.log
23-
24-# Tests
25-.coverage
26-.pytest_cache/
27-
28-/app
2+*.egg-info/
3+__pycache__/
294
+0-0db.sqlite3

Binary file changed.

+0-0example_app/__init__.py

No content changes (mode or rename only).

+8-0example_app/admin.py
@@ -0,0 +1,8 @@
1+from django.contrib import admin
2+
3+from example_app.models import Post
4+
5+
6+@admin.register(Post)
7+class PostAdmin(admin.ModelAdmin):
8+ list_display = ["lastUpdated", "text"]
+17-0example_app/apps.py
@@ -0,0 +1,17 @@
1+from django.apps import AppConfig
2+from django.core.management import call_command
3+from django.db.models.signals import post_migrate
4+
5+
6+class ExampleAppConfig(AppConfig):
7+ default_auto_field = "django.db.models.BigAutoField"
8+ name = "example_app"
9+
10+ def ready(self):
11+ print("HELLO")
12+ post_migrate.connect(load_fixtures, sender=self)
13+
14+
15+def load_fixtures(**_):
16+ print("Making init data")
17+ call_command("loaddata", "initial_data", app_label="example_app", verbosity=0)
+30-0example_app/fixtures/initial_data.json
@@ -0,0 +1,30 @@
1+[
2+ {
3+ "model": "auth.user",
4+ "pk": 1,
5+ "fields": {
6+ "username": "admin",
7+ "password": "pbkdf2_sha256$1000000$98yGPuJFQz9A7CNYptr34U$QvbT91v0BxBzjtuD7ILuf+RG0B7ULTvdNsNXWWpcwNY=",
8+ "is_superuser": true,
9+ "is_staff": true,
10+ "is_active": true,
11+ "email": "admin@example.com"
12+ }
13+ },
14+ {
15+ "model": "example_app.post",
16+ "pk": 1,
17+ "fields": {
18+ "lastUpdated": "2024-01-01T12:00:00Z",
19+ "text": "Hello world!"
20+ }
21+ },
22+ {
23+ "model": "example_app.post",
24+ "pk": 2,
25+ "fields": {
26+ "lastUpdated": "2024-01-02T15:00:00Z",
27+ "text": "Second post"
28+ }
29+ }
30+]
+22-0example_app/migrations/0001_initial.py
@@ -0,0 +1,22 @@
1+# Generated by Django 5.2.1 on 2025-06-03 16:37
2+
3+from django.db import migrations, models
4+
5+
6+class Migration(migrations.Migration):
7+
8+ initial = True
9+
10+ dependencies = [
11+ ]
12+
13+ operations = [
14+ migrations.CreateModel(
15+ name='Post',
16+ fields=[
17+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+ ('lastUpdated', models.DateTimeField()),
19+ ('text', models.TextField()),
20+ ],
21+ ),
22+ ]
+0-0example_app/migrations/__init__.py

No content changes (mode or rename only).

+6-0example_app/models.py
@@ -0,0 +1,6 @@
1+from django.db import models
2+
3+
4+class Post(models.Model):
5+ lastUpdated = models.DateTimeField()
6+ text = models.TextField()
+3-0example_app/tests.py
@@ -0,0 +1,3 @@
1+from django.test import TestCase
2+
3+# Create your tests here.
+5-0example_app/urls.py
@@ -0,0 +1,5 @@
1+from django.urls import path
2+
3+from example_app.views import View
4+
5+urlpatterns = [path("", View.as_view())]
+25-0example_app/views.py
@@ -0,0 +1,25 @@
1+from typing import Union
2+from rest_framework.request import Request
3+from rest_framework.response import Response
4+from example_app.models import Post
5+from plusrequest.request import PlusRequest
6+from rest_framework.views import APIView
7+
8+
9+class View(APIView):
10+ def get(self, request: PlusRequest):
11+ instance = Post.objects.all().first()
12+ if instance:
13+ request.top_builder(instance=instance).raise_get()
14+ return Response(
15+ {
16+ "CONTENT_LENGTH": request.META.get("CONTENT_LENGTH"),
17+ "HTTP_ACCEPT": request.META.get("HTTP_ACCEPT"),
18+ }
19+ )
20+
21+ def put(self, request: PlusRequest):
22+ instance = Post.objects.filter(request.data.get("id", None)).first()
23+ if instance:
24+ request.top_builder(instance=instance).raise_update()
25+ return Response({"port": request.META.get("SERVER_PORT")})
+0-0example_project/__init__.py

No content changes (mode or rename only).

+16-0example_project/asgi.py
@@ -0,0 +1,16 @@
1+"""
2+ASGI config for example project.
3+
4+It exposes the ASGI callable as a module-level variable named ``application``.
5+
6+For more information on this file, see
7+https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
8+"""
9+
10+import os
11+
12+from django.core.asgi import get_asgi_application
13+
14+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
15+
16+application = get_asgi_application()
+22-0example_project/migrations/0001_initial.py
@@ -0,0 +1,22 @@
1+# Generated by Django 5.2.1 on 2025-06-03 16:04
2+
3+from django.db import migrations, models
4+
5+
6+class Migration(migrations.Migration):
7+
8+ initial = True
9+
10+ dependencies = [
11+ ]
12+
13+ operations = [
14+ migrations.CreateModel(
15+ name='Post',
16+ fields=[
17+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18+ ('lastUpdated', models.DateTimeField()),
19+ ('text', models.TextField()),
20+ ],
21+ ),
22+ ]
+0-0example_project/migrations/__init__.py

No content changes (mode or rename only).

+126-0example_project/settings.py
@@ -0,0 +1,126 @@
1+"""
2+Django settings for example project.
3+
4+Generated by 'django-admin startproject' using Django 5.2.1.
5+
6+For more information on this file, see
7+https://docs.djangoproject.com/en/5.2/topics/settings/
8+
9+For the full list of settings and their values, see
10+https://docs.djangoproject.com/en/5.2/ref/settings/
11+"""
12+
13+from pathlib import Path
14+
15+# Build paths inside the project like this: BASE_DIR / 'subdir'.
16+BASE_DIR = Path(__file__).resolve().parent.parent
17+
18+
19+# Quick-start development settings - unsuitable for production
20+# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
21+
22+# SECURITY WARNING: keep the secret key used in production secret!
23+SECRET_KEY = "django-insecure-=%r#z55prje)c3*m*e(xim*&k3)4m=4lqljp)4j)k+5ppk(fz6"
24+
25+# SECURITY WARNING: don't run with debug turned on in production!
26+DEBUG = True
27+
28+ALLOWED_HOSTS = []
29+
30+
31+# Application definition
32+
33+INSTALLED_APPS = [
34+ "django.contrib.admin",
35+ "django.contrib.auth",
36+ "django.contrib.contenttypes",
37+ "django.contrib.sessions",
38+ "django.contrib.messages",
39+ "django.contrib.staticfiles",
40+ "rest_framework",
41+ "example_app",
42+ "plusrequest",
43+]
44+
45+MIDDLEWARE = [
46+ "django.middleware.security.SecurityMiddleware",
47+ "django.contrib.sessions.middleware.SessionMiddleware",
48+ "django.middleware.common.CommonMiddleware",
49+ "django.middleware.csrf.CsrfViewMiddleware",
50+ "django.contrib.auth.middleware.AuthenticationMiddleware",
51+ "django.contrib.messages.middleware.MessageMiddleware",
52+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
53+ "plusrequest.middleware.PlusRequestMiddleware",
54+]
55+
56+ROOT_URLCONF = "example_project.urls"
57+
58+TEMPLATES = [
59+ {
60+ "BACKEND": "django.template.backends.django.DjangoTemplates",
61+ "DIRS": [],
62+ "APP_DIRS": True,
63+ "OPTIONS": {
64+ "context_processors": [
65+ "django.template.context_processors.request",
66+ "django.contrib.auth.context_processors.auth",
67+ "django.contrib.messages.context_processors.messages",
68+ ],
69+ },
70+ },
71+]
72+
73+WSGI_APPLICATION = "example_project.wsgi.application"
74+
75+
76+# Database
77+# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
78+
79+DATABASES = {
80+ "default": {
81+ "ENGINE": "django.db.backends.sqlite3",
82+ "NAME": BASE_DIR / "db.sqlite3",
83+ }
84+}
85+
86+
87+# Password validation
88+# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
89+
90+AUTH_PASSWORD_VALIDATORS = [
91+ {
92+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
93+ },
94+ {
95+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
96+ },
97+ {
98+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
99+ },
100+ {
101+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
102+ },
103+]
104+
105+
106+# Internationalization
107+# https://docs.djangoproject.com/en/5.2/topics/i18n/
108+
109+LANGUAGE_CODE = "en-us"
110+
111+TIME_ZONE = "UTC"
112+
113+USE_I18N = True
114+
115+USE_TZ = True
116+
117+
118+# Static files (CSS, JavaScript, Images)
119+# https://docs.djangoproject.com/en/5.2/howto/static-files/
120+
121+STATIC_URL = "static/"
122+
123+# Default primary key field type
124+# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
125+
126+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
+22-0example_project/urls.py
@@ -0,0 +1,22 @@
1+"""
2+URL configuration for example project.
3+
4+The `urlpatterns` list routes URLs to views. For more information please see:
5+ https://docs.djangoproject.com/en/5.2/topics/http/urls/
6+Examples:
7+Function views
8+ 1. Add an import: from my_app import views
9+ 2. Add a URL to urlpatterns: path('', views.home, name='home')
10+Class-based views
11+ 1. Add an import: from other_app.views import Home
12+ 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
13+Including another URLconf
14+ 1. Import the include() function: from django.urls import include, path
15+ 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
16+"""
17+
18+from django.contrib import admin
19+from django.urls import include, path
20+
21+
22+urlpatterns = [path("admin/", admin.site.urls), path("", include("example_app.urls"))]
+16-0example_project/wsgi.py
@@ -0,0 +1,16 @@
1+"""
2+WSGI config for example project.
3+
4+It exposes the WSGI callable as a module-level variable named ``application``.
5+
6+For more information on this file, see
7+https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
8+"""
9+
10+import os
11+
12+from django.core.wsgi import get_wsgi_application
13+
14+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
15+
16+application = get_wsgi_application()
+23-0manage.py
@@ -0,0 +1,23 @@
1+#!/usr/bin/env python
2+"""Django's command-line utility for administrative tasks."""
3+
4+import os
5+import sys
6+
7+
8+def main():
9+ """Run administrative tasks."""
10+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_project.settings")
11+ try:
12+ from django.core.management import execute_from_command_line
13+ except ImportError as exc:
14+ raise ImportError(
15+ "Couldn't import Django. Are you sure it's installed and "
16+ "available on your PYTHONPATH environment variable? Did you "
17+ "forget to activate a virtual environment?"
18+ ) from exc
19+ execute_from_command_line(sys.argv)
20+
21+
22+if __name__ == "__main__":
23+ main()
+3-4plusrequest/middleware.py
@@ -1,5 +1,4 @@
1-from rest_framework.request import Request
2-from plusrequest.request import PlusRequest
1+from plusrequest.timestamp_op import TimestampOp
32
43
54 class PlusRequestMiddleware:
@@ -7,12 +6,12 @@ class PlusRequestMiddleware:
76 self.get_response = get_response
87 # One-time configuration and initialization.
98
10- def __call__(self, request: Request):
9+ def __call__(self, request):
1110 # Code to be executed for each request before
1211 # the view (and later middleware) are called.
1312
1413 # Add header device date functionality to request
15- request = PlusRequest._promote_drf_to_plus_request(request)
14+ request.top_builder = TimestampOp(request)
1615
1716 # Ensure the member shortcut exists
1817 response = self.get_response(request)
+2-6plusrequest/request.py
@@ -1,13 +1,14 @@
11 from typing import (
22 TYPE_CHECKING,
33 )
4+from django.http import HttpRequest
45 from rest_framework.request import Request
56
67 from plusrequest.meta import Meta
78 from plusrequest.timestamp_op import TimestampOp
89
910
10-class PlusRequest(Request):
11+class PlusRequest(Request, HttpRequest):
1112 if TYPE_CHECKING:
1213 META: Meta
1314
@@ -22,8 +23,3 @@ class PlusRequest(Request):
2223 ...
2324 return Response(data)
2425 ```"""
25- return TimestampOp(self)
26-
27- @classmethod
28- def _promote_drf_to_plus_request(cls, request: Request):
29- return cls(request._request)
+3-5plusrequest/timestamp_op.py
@@ -1,6 +1,6 @@
11 import datetime as dt
2-from typing import TYPE_CHECKING
32 from django.db import models
3+from rest_framework.request import Request
44
55 from plusrequest.exceptions import (
66 InvalidClientDatetimeField,
@@ -10,9 +10,6 @@ from plusrequest.exceptions import (
1010 from plusrequest.settings import conf
1111 from plusrequest.types import MissingAction
1212
13-if TYPE_CHECKING:
14- from plusrequest.request import PlusRequest
15-
1613
1714 class TimestampOp:
1815 """Builds a set of instructions on how to interpret client and server timestamps.
@@ -21,7 +18,7 @@ class TimestampOp:
2118
2219 _missing_action: MissingAction
2320
24- def __init__(self, parent: "PlusRequest"):
21+ def __init__(self, parent: Request):
2522 self.parent = parent
2623
2724 def __call__(
@@ -72,6 +69,7 @@ class TimestampOp:
7269 raise InvalidClientDatetimeField(
7370 self._body_field, body_str, self._datetime_format, 400
7471 )
72+ return self
7573
7674 def raise_get(
7775 self, code: str | int | None = None, missing_action: MissingAction | None = None
+4-2pyproject.toml
@@ -9,8 +9,10 @@ dependencies = ["django"]
99 [project.urls]
1010 Homepage = "https://github.com/huncholane/django-smartwatch"
1111
12-[tool.setuptools]
13-packages = ["plusrequest"]
12+[tool.setuptools.packages.find]
13+include = ["plusrequest"]
14+where = ["."]
15+
1416 [build-system]
1517 requires = ["setuptools"]
1618 build-backend = "setuptools.build_meta"