1
0
mirror of https://github.com/etesync/server synced 2025-04-23 19:59:03 +00:00

Compare commits

...

46 Commits

Author SHA1 Message Date
Tom Hacohen
6ac5412104 Bump django version. 2024-07-12 08:44:19 -04:00
Tom Hacohen
cc54a136f1 Fix server error when passing null collection types. 2024-06-29 14:55:48 -04:00
Tom Hacohen
386c01d89e Bump version and update changelog. 2024-06-13 14:02:33 -04:00
Tom Hacohen
f35b4b94e5 Make more fields truely optional (behavior change in pydantic v2). 2024-06-13 13:50:38 -04:00
Craeckie
19aba5345a
added CSRF_TRUSTED_ORIGINS (#183)
Since some recent upgrade, I'm not able to login to the admin page of etesync (`/admin/login/`), because the CSRF check fails.


After adding `CSRF_TRUSTED_ORIGINS = ['https://my-domain.com']`, it works.
According to the [docs](https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins), this setting is required in addition to `ALLOWED_HOSTS`.
2024-06-09 23:27:49 +03:00
Tom Hacohen
2deb59e932 Bump version and update changelog. 2024-06-09 11:21:24 -04:00
Tom Hacohen
c9b8f43845 Set default auto field to appease django warning. 2024-06-09 11:15:41 -04:00
Tom Hacohen
f1c072bd0a Fix issue with how we render binary 422 errors
json_encoder assumes it's not binary, but our error messages may include
some binary stuff.
2024-06-08 22:54:02 -04:00
Tom Hacohen
043dc972ae Bump version and update changelog. 2024-06-08 21:52:36 -04:00
Tom Hacohen
8f588af665
Merge PR #184: Upgrade FastAPI and Pydantic to most recent versions 2024-06-08 20:50:42 -04:00
Tom Hacohen
d7075c0169 Mark optional field as optional. 2024-06-08 20:49:13 -04:00
Tom Hacohen
a27ce2f4d0 Also handle 422 as msgpack. 2024-06-08 20:41:01 -04:00
Tom Hacohen
138d99dd7f Update code to adjust to most recent python/fastapi. 2024-06-08 20:27:33 -04:00
Tom Hacohen
0be14a7b0e Fixes for fastapi. 2024-06-08 20:17:02 -04:00
Tom Hacohen
57e676baa1 Adjust to fastapi changes. 2024-06-08 19:52:10 -04:00
Tom Hacohen
b9f2cea951 more 2024-06-08 19:29:56 -04:00
Tom Hacohen
0d9c9f153d Type fix. 2024-06-08 19:29:56 -04:00
Tom Hacohen
49eeeefef5 Make fastapi 0.104 and pydantic 2.0 min requirements 2024-06-08 19:29:56 -04:00
Tom Hacohen
fb9cc701d0 Adjust pydantic code to v2. 2024-06-08 18:10:34 -04:00
Tom Hacohen
0cdab19308 Fix rust complaints. 2024-06-08 18:04:59 -04:00
Tom Hacohen
79d28586c5 Run ruff format. 2024-06-08 17:51:44 -04:00
Tom Hacohen
df0d1596e2 Upgrade dev deps and add ruff. 2024-06-08 17:49:40 -04:00
Tom Hacohen
dae6f17355 Upgrade dependencies. 2024-06-08 17:45:45 -04:00
Tom Hacohen
45d2696e21 Bump version and update changelog. 2024-06-04 08:20:11 -04:00
Tom Hacohen
e6750d7a78
Merge pull request #182 from pyrox0/master
Fix django version specifier
2024-06-03 14:15:37 -04:00
Pyrox
b3e09e77bf
Fix django version specifier
This errors out when building. I've patched this in Nixpkgs, but an
upstream patch is much better for others.
2024-06-03 14:03:31 -04:00
Tom Hacohen
6aa1e299ec Bump version and update changelog. 2024-06-02 21:39:03 -04:00
Tom Hacohen
da613b4700 Bump django to version 4 LTS
Apparently version 3 is EOL.

Fixes #173
2024-06-02 21:37:29 -04:00
Tom Hacohen
d9be3437f5 Bump version and update changelog. 2024-06-02 21:18:26 -04:00
Tom Hacohen
62152ce52b
Merge pull request #147 from Xiretza/settings-env-override
fix(settings): ensure environment variables always override config
2024-06-02 21:13:14 -04:00
Tommi Mäklin
6b66ec16d5 Update pyyaml from 6.0 to 6.0.1. 2023-10-20 09:22:45 -04:00
Tom Hacohen
a54afd5210 Optimize stoken-using functions to only account for current revisions.
No need to account for revisions that are not current when calculating stokens
because those, by definition, are not the latest ones, and therefore won't have
the most recent stokens.

This becomes a problem when collections have many associated revisions.
2023-08-15 20:21:23 -04:00
Alejandro
4293acb3a3 fix: Python files 2023-07-24 22:06:22 -04:00
LuPa
55d3fb7e8e Update README.md
Add static files creation
2023-02-17 07:23:19 -08:00
Tom Hacohen
9aaea7b6a7
README: add Kanaye to contributors. 2022-12-12 01:33:41 +02:00
Tom Hacohen
0bd40807ba Bump version and update changelog. 2022-12-09 09:03:08 -05:00
Tom Hacohen
d843d580eb
Merge pull request #159 from victor-rds/py3.11
Update dependencies for Python 3.11
2022-12-07 16:43:41 -05:00
Victor R. Santos
a48f37c0c9
Update testserver base image
Use Python 3.11.0-alpine image to test the up-to-date dependencies.
2022-12-07 15:49:15 -03:00
Victor R. Santos
f9645917d7
Update dependencies for Python 3.11
Fix `httptools` and `uvloop` failing to build using on Python 3.11
Updated all other dependencies.
2022-12-07 15:30:27 -03:00
Tom Hacohen
4bf81f49ad Bump version and update changelog. 2022-10-04 12:31:38 -04:00
Tom Hacohen
c61dd86a8c
Merge: Replace aioredis with redis-py (#151) 2022-09-04 13:59:50 -04:00
Tom Hacohen
8c6d04e8d3 Replace aioredis with redis-py
aioredis has been merged into redis-py and will no longer be maintained
as a separate project.
2022-09-04 19:57:27 +02:00
Xiretza
c4475535ea fix(settings): ensure environment variables always override config
DJANGO_STATIC_ROOT is required in order to run `collectstatic` in
a packaging environment - manually specifying it as an environment
variable should always override any config files that may exist.
2022-07-21 12:37:43 +02:00
Tom Hacohen
2f1f95fea9 Optimize how we fetch the latest (current) revision
The way were were doing it was implicitly sorting the query items and it
was causing millions of items to be sorted (even though the result should
only have one) making it slow.

By switching away from `get()` and `first()` we are telling django to
not try to sort.
2022-06-19 22:25:49 +03:00
Tom Hacohen
5f455e55b5 Bump version and update changelog. 2022-06-13 14:02:48 +03:00
Tom Hacohen
709a607d47 Update Django dependency. 2022-06-13 14:01:15 +03:00
39 changed files with 467 additions and 332 deletions

View File

@ -1,5 +1,36 @@
# Changelog # Changelog
## Version 0.14.2
- Fix issue with some requests failing in some scenarios with the JS client.
- The JS client was omitting optional fields which were accidentally made to be required. It happened because pydantic v2 changed the behavior in a few ways (for the better fwiw) and we missed a few places when upgrading.
## Version 0.14.1
- Fix issue with serializing non utf8 422 errors
- Appease django warnings about default auto field
## Version 0.14.0
- Upgrade to latest FastAPI and Pydantic for compatibility with Python 3.12
## Version 0.13.1
- Fix requirements definition for django.
## Version 0.13.0
- Update Django dependency to 4 (LTS)
## Version 0.12.0
- Optimize some database queries fixing performance degradation with large installations.
- Ensure environment variables always override config
## Version 0.11.0
- Update deps for Python 3.11
## Version 0.10.0
- Replace the deprecated aioredis with redis-py
- Optimize how we fetch the current (latest) revision of items
## Version 0.9.1
- Update pinned Django version (only matters if using `requirements.txt`).
## Version 0.9.0 ## Version 0.9.0
- Add LDAP support for checking the validity of a username - Add LDAP support for checking the validity of a username
- Allow specifying engine-specific database options - Allow specifying engine-specific database options

View File

@ -63,6 +63,12 @@ Now you can initialise our django app.
./manage.py migrate ./manage.py migrate
``` ```
Create static files:
```
./manage.py collectstatic
```
And you are done! You can now run the debug server just to see everything works as expected by running: And you are done! You can now run the debug server just to see everything works as expected by running:
``` ```
@ -178,3 +184,4 @@ Become a financial contributor and help us sustain our community!
[![ilovept](https://github.com/ilovept.png?size=40)](https://github.com/ilovept) [![ilovept](https://github.com/ilovept.png?size=40)](https://github.com/ilovept)
[![ryanleesipes](https://github.com/ryanleesipes.png?size=40)](https://github.com/ryanleesipes) [![ryanleesipes](https://github.com/ryanleesipes.png?size=40)](https://github.com/ryanleesipes)
[![DanielG](https://github.com/DanielG.png?size=40)](https://github.com/DanielG) [![DanielG](https://github.com/DanielG.png?size=40)](https://github.com/DanielG)
[![Kanaye](https://github.com/Kanaye.png?size=40)](https://github.com/Kanaye)

View File

@ -1,4 +1,4 @@
FROM python:3.9.0-alpine FROM python:3.11.0-alpine
ARG ETESYNC_VERSION ARG ETESYNC_VERSION

View File

@ -1 +1,3 @@
from .app_settings_inner import app_settings from .app_settings_inner import app_settings
__all__ = ["app_settings"]

View File

@ -34,11 +34,11 @@ class AppSettings:
return getattr(settings, self.prefix + name, dflt) return getattr(settings, self.prefix + name, dflt)
@cached_property @cached_property
def REDIS_URI(self) -> t.Optional[str]: # pylint: disable=invalid-name def REDIS_URI(self) -> t.Optional[str]: # noqa: N802
return self._setting("REDIS_URI", None) return self._setting("REDIS_URI", None)
@cached_property @cached_property
def API_PERMISSIONS_READ(self): # pylint: disable=invalid-name def API_PERMISSIONS_READ(self): # noqa: N802
perms = self._setting("API_PERMISSIONS_READ", tuple()) perms = self._setting("API_PERMISSIONS_READ", tuple())
ret = [] ret = []
for perm in perms: for perm in perms:
@ -46,7 +46,7 @@ class AppSettings:
return ret return ret
@cached_property @cached_property
def API_PERMISSIONS_WRITE(self): # pylint: disable=invalid-name def API_PERMISSIONS_WRITE(self): # noqa: N802
perms = self._setting("API_PERMISSIONS_WRITE", tuple()) perms = self._setting("API_PERMISSIONS_WRITE", tuple())
ret = [] ret = []
for perm in perms: for perm in perms:
@ -54,35 +54,35 @@ class AppSettings:
return ret return ret
@cached_property @cached_property
def GET_USER_QUERYSET_FUNC(self): # pylint: disable=invalid-name def GET_USER_QUERYSET_FUNC(self): # noqa: N802
get_user_queryset = self._setting("GET_USER_QUERYSET_FUNC", None) get_user_queryset = self._setting("GET_USER_QUERYSET_FUNC", None)
if get_user_queryset is not None: if get_user_queryset is not None:
return self.import_from_str(get_user_queryset) return self.import_from_str(get_user_queryset)
return None return None
@cached_property @cached_property
def CREATE_USER_FUNC(self): # pylint: disable=invalid-name def CREATE_USER_FUNC(self): # noqa: N802
func = self._setting("CREATE_USER_FUNC", None) func = self._setting("CREATE_USER_FUNC", None)
if func is not None: if func is not None:
return self.import_from_str(func) return self.import_from_str(func)
return None return None
@cached_property @cached_property
def DASHBOARD_URL_FUNC(self): # pylint: disable=invalid-name def DASHBOARD_URL_FUNC(self): # noqa: N802
func = self._setting("DASHBOARD_URL_FUNC", None) func = self._setting("DASHBOARD_URL_FUNC", None)
if func is not None: if func is not None:
return self.import_from_str(func) return self.import_from_str(func)
return None return None
@cached_property @cached_property
def CHUNK_PATH_FUNC(self): # pylint: disable=invalid-name def CHUNK_PATH_FUNC(self): # noqa: N802
func = self._setting("CHUNK_PATH_FUNC", None) func = self._setting("CHUNK_PATH_FUNC", None)
if func is not None: if func is not None:
return self.import_from_str(func) return self.import_from_str(func)
return None return None
@cached_property @cached_property
def CHALLENGE_VALID_SECONDS(self): # pylint: disable=invalid-name def CHALLENGE_VALID_SECONDS(self): # noqa: N802
return self._setting("CHALLENGE_VALID_SECONDS", 60) return self._setting("CHALLENGE_VALID_SECONDS", 60)

View File

@ -15,22 +15,21 @@
import typing as t import typing as t
from pathlib import Path from pathlib import Path
from django.db import models, transaction
from django.conf import settings from django.conf import settings
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db.models import Max, Value as V from django.db import models, transaction
from django.db.models import Max, Value as Val
from django.db.models.functions import Coalesce, Greatest from django.db.models.functions import Coalesce, Greatest
from django.utils.functional import cached_property
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from . import app_settings from . import app_settings
UidValidator = RegexValidator(regex=r"^[a-zA-Z0-9\-_]{20,}$", message="Not a valid UID") UidValidator = RegexValidator(regex=r"^[a-zA-Z0-9\-_]{20,}$", message="Not a valid UID")
def stoken_annotation_builder(stoken_id_fields: t.List[str]): def stoken_annotation_builder(stoken_id_fields: t.List[str]):
aggr_fields = [Coalesce(Max(field), V(0)) for field in stoken_id_fields] aggr_fields = [Coalesce(Max(field), Val(0)) for field in stoken_id_fields]
return Greatest(*aggr_fields) if len(aggr_fields) > 1 else aggr_fields[0] return Greatest(*aggr_fields) if len(aggr_fields) > 1 else aggr_fields[0]
@ -66,7 +65,7 @@ class Collection(models.Model):
@cached_property @cached_property
def stoken(self) -> str: def stoken(self) -> str:
stoken_id = ( stoken_id = (
self.__class__.objects.filter(main_item=self.main_item) self.__class__.objects.filter(main_item=self.main_item, items__revisions__current=True)
.annotate(max_stoken=self.stoken_annotation) .annotate(max_stoken=self.stoken_annotation)
.values("max_stoken") .values("max_stoken")
.first()["max_stoken"] .first()["max_stoken"]
@ -96,7 +95,7 @@ class CollectionItem(models.Model):
@cached_property @cached_property
def content(self) -> "CollectionItemRevision": def content(self) -> "CollectionItemRevision":
return self.revisions.get(current=True) return self.revisions.filter(current=True)[0]
@property @property
def etag(self) -> str: def etag(self) -> str:

View File

@ -1,6 +1,7 @@
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from etebase_server.myauth.models import get_typed_user_model from etebase_server.myauth.models import get_typed_user_model
User = get_typed_user_model() User = get_typed_user_model()
@ -15,7 +16,6 @@ def get_default_expiry():
class AuthToken(models.Model): class AuthToken(models.Model):
key = models.CharField(max_length=40, unique=True, db_index=True, default=generate_key) key = models.CharField(max_length=40, unique=True, db_index=True, default=generate_key)
user = models.ForeignKey(User, null=False, blank=False, related_name="auth_token_set", on_delete=models.CASCADE) user = models.ForeignKey(User, null=False, blank=False, related_name="auth_token_set", on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)

View File

@ -1,13 +1,13 @@
import typing as t import typing as t
from dataclasses import dataclass from dataclasses import dataclass
from django.db.models import QuerySet
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db.models import QuerySet
from etebase_server.myauth.models import UserType, get_typed_user_model from etebase_server.myauth.models import UserType, get_typed_user_model
from . import app_settings from . import app_settings
User = get_typed_user_model() User = get_typed_user_model()

View File

@ -2,6 +2,7 @@
FIXME: this whole function is a hack around the django db limitations due to how db connections are cached and cleaned. FIXME: this whole function is a hack around the django db limitations due to how db connections are cached and cleaned.
Essentially django assumes there's the django request dispatcher to automatically clean up after the ORM. Essentially django assumes there's the django request dispatcher to automatically clean up after the ORM.
""" """
import typing as t import typing as t
from functools import wraps from functools import wraps

View File

@ -1,18 +1,17 @@
import dataclasses import dataclasses
from django.db.models import QuerySet
from django.utils import timezone
from fastapi import Depends from fastapi import Depends
from fastapi.security import APIKeyHeader from fastapi.security import APIKeyHeader
from django.utils import timezone
from django.db.models import QuerySet
from etebase_server.django import models from etebase_server.django import models
from etebase_server.django.token_auth.models import AuthToken, get_default_expiry from etebase_server.django.token_auth.models import AuthToken, get_default_expiry
from etebase_server.myauth.models import UserType, get_typed_user_model from etebase_server.myauth.models import UserType, get_typed_user_model
from .db_hack import django_db_cleanup_decorator
from .exceptions import AuthenticationFailed from .exceptions import AuthenticationFailed
from .utils import get_object_or_404 from .utils import get_object_or_404
from .db_hack import django_db_cleanup_decorator
User = get_typed_user_model() User = get_typed_user_model()
token_scheme = APIKeyHeader(name="Authorization") token_scheme = APIKeyHeader(name="Authorization")

View File

@ -1,8 +1,8 @@
from fastapi import status, HTTPException
import typing as t import typing as t
from pydantic import BaseModel
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
from fastapi import HTTPException, status
from pydantic import BaseModel
class HttpErrorField(BaseModel): class HttpErrorField(BaseModel):
@ -11,7 +11,7 @@ class HttpErrorField(BaseModel):
detail: str detail: str
class Config: class Config:
orm_mode = True from_attributes = True
class HttpErrorOut(BaseModel): class HttpErrorOut(BaseModel):
@ -20,7 +20,7 @@ class HttpErrorOut(BaseModel):
errors: t.Optional[t.List[HttpErrorField]] errors: t.Optional[t.List[HttpErrorField]]
class Config: class Config:
orm_mode = True from_attributes = True
class CustomHttpException(HTTPException): class CustomHttpException(HTTPException):

View File

@ -1,19 +1,20 @@
import typing as t
from django.conf import settings from django.conf import settings
# Not at the top of the file because we first need to setup django # Not at the top of the file because we first need to setup django
from fastapi import FastAPI, Request from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from etebase_server.django import app_settings
from .exceptions import CustomHttpException from .exceptions import CustomHttpException
from .msgpack import MsgpackResponse from .msgpack import MsgpackResponse
from .routers.authentication import authentication_router from .routers.authentication import authentication_router
from .routers.collection import collection_router, item_router from .routers.collection import collection_router, item_router
from .routers.member import member_router
from .routers.invitation import invitation_incoming_router, invitation_outgoing_router from .routers.invitation import invitation_incoming_router, invitation_outgoing_router
from .routers.member import member_router
from .routers.websocket import websocket_router from .routers.websocket import websocket_router
@ -24,12 +25,12 @@ def create_application(prefix="", middlewares=[]):
externalDocs={ externalDocs={
"url": "https://docs.etebase.com", "url": "https://docs.etebase.com",
"description": "Docs about the API specifications and clients.", "description": "Docs about the API specifications and clients.",
} },
# FIXME: version="2.5.0", # FIXME: version="2.5.0",
) )
VERSION = "v1" VERSION = "v1" # noqa: N806
BASE_PATH = f"{prefix}/api/{VERSION}" BASE_PATH = f"{prefix}/api/{VERSION}" # noqa: N806
COLLECTION_UID_MARKER = "{collection_uid}" COLLECTION_UID_MARKER = "{collection_uid}" # noqa: N806
app.include_router(authentication_router, prefix=f"{BASE_PATH}/authentication", tags=["authentication"]) app.include_router(authentication_router, prefix=f"{BASE_PATH}/authentication", tags=["authentication"])
app.include_router(collection_router, prefix=f"{BASE_PATH}/collection", tags=["collection"]) app.include_router(collection_router, prefix=f"{BASE_PATH}/collection", tags=["collection"])
app.include_router(item_router, prefix=f"{BASE_PATH}/collection/{COLLECTION_UID_MARKER}", tags=["item"]) app.include_router(item_router, prefix=f"{BASE_PATH}/collection/{COLLECTION_UID_MARKER}", tags=["item"])
@ -75,6 +76,16 @@ def create_application(prefix="", middlewares=[]):
async def custom_exception_handler(request: Request, exc: CustomHttpException): async def custom_exception_handler(request: Request, exc: CustomHttpException):
return MsgpackResponse(status_code=exc.status_code, content=exc.as_dict) return MsgpackResponse(status_code=exc.status_code, content=exc.as_dict)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
from pydantic import TypeAdapter
errors = TypeAdapter(t.Dict[str, t.Any])
return MsgpackResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=errors.dump_python({"detail": exc.errors()}),
)
app.mount(settings.STATIC_URL, StaticFiles(directory=settings.STATIC_ROOT), name="static") app.mount(settings.STATIC_URL, StaticFiles(directory=settings.STATIC_ROOT), name="static")
return app return app

View File

@ -5,16 +5,19 @@ from pydantic import BaseModel
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import Response from starlette.responses import Response
from .utils import msgpack_encode, msgpack_decode
from .db_hack import django_db_cleanup_decorator from .db_hack import django_db_cleanup_decorator
from .utils import msgpack_decode, msgpack_encode
class MsgpackRequest(Request): class MsgpackRequest(Request):
media_type = "application/msgpack" media_type = "application/msgpack"
async def raw_body(self) -> bytes:
return await super().body()
async def body(self) -> bytes: async def body(self) -> bytes:
if not hasattr(self, "_json"): if not hasattr(self, "_json"):
body = await super().body() body = await self.raw_body()
self._json = msgpack_decode(body) self._json = msgpack_decode(body)
return self._json return self._json
@ -27,7 +30,7 @@ class MsgpackResponse(Response):
return b"" return b""
if isinstance(content, BaseModel): if isinstance(content, BaseModel):
content = content.dict() content = content.model_dump()
return msgpack_encode(content) return msgpack_encode(content)
@ -48,7 +51,7 @@ class MsgpackRoute(APIRoute):
status_code=self.status_code, status_code=self.status_code,
# use custom response class or fallback on default self.response_class # use custom response class or fallback on default self.response_class
response_class=self.ROUTES_HANDLERS_CLASSES.get(media_type, self.response_class), response_class=self.ROUTES_HANDLERS_CLASSES.get(media_type, self.response_class),
response_field=self.secure_cloned_response_field, response_field=self.response_field,
response_model_include=self.response_model_include, response_model_include=self.response_model_include,
response_model_exclude=self.response_model_exclude, response_model_exclude=self.response_model_exclude,
response_model_by_alias=self.response_model_by_alias, response_model_by_alias=self.response_model_by_alias,
@ -60,14 +63,14 @@ class MsgpackRoute(APIRoute):
def get_route_handler(self) -> t.Callable: def get_route_handler(self) -> t.Callable:
async def custom_route_handler(request: Request) -> Response: async def custom_route_handler(request: Request) -> Response:
content_type = request.headers.get("Content-Type") content_type = request.headers.get("Content-Type")
try: if content_type is not None:
request_cls = self.REQUESTS_CLASSES[content_type] try:
request = request_cls(request.scope, request.receive) request_cls = self.REQUESTS_CLASSES[content_type]
except KeyError: request = request_cls(request.scope, request.receive)
# nothing registered to handle content_type, process given requests as-is except KeyError:
pass # nothing registered to handle content_type, process given requests as-is
pass
accept = request.headers.get("Accept") accept = request.headers.get("Accept")
route_handler = self._get_media_type_route_handler(accept) route_handler = self._get_media_type_route_handler(accept)

View File

@ -1,5 +1,6 @@
import typing as t import typing as t
import aioredis
from redis import asyncio as aioredis
from etebase_server.django import app_settings from etebase_server.django import app_settings
@ -12,12 +13,11 @@ class RedisWrapper:
async def setup(self): async def setup(self):
if self.redis_uri is not None: if self.redis_uri is not None:
self.redis = await aioredis.create_redis_pool(self.redis_uri) self.redis = await aioredis.from_url(self.redis_uri)
async def close(self): async def close(self):
if hasattr(self, "redis"): if hasattr(self, "redis"):
self.redis.close() await self.redis.close()
await self.redis.wait_closed()
@property @property
def is_active(self): def is_active(self):

View File

@ -1,5 +1,4 @@
import typing as t import typing as t
from typing_extensions import Literal
from datetime import datetime from datetime import datetime
import nacl import nacl
@ -8,22 +7,24 @@ import nacl.hash
import nacl.secret import nacl.secret
import nacl.signing import nacl.signing
from django.conf import settings from django.conf import settings
from django.contrib.auth import user_logged_out, user_logged_in from django.contrib.auth import user_logged_in, user_logged_out
from django.core import exceptions as django_exceptions from django.core import exceptions as django_exceptions
from django.db import transaction from django.db import transaction
from django.utils.functional import cached_property from django.utils.functional import cached_property
from fastapi import APIRouter, Depends, status, Request from fastapi import APIRouter, Depends, Request, status
from typing_extensions import Literal
from etebase_server.django import app_settings, models from etebase_server.django import app_settings, models
from etebase_server.django.token_auth.models import AuthToken
from etebase_server.django.models import UserInfo from etebase_server.django.models import UserInfo
from etebase_server.django.signals import user_signed_up from etebase_server.django.signals import user_signed_up
from etebase_server.django.utils import create_user, get_user_queryset, CallbackContext from etebase_server.django.token_auth.models import AuthToken
from etebase_server.django.utils import CallbackContext, create_user, get_user_queryset
from etebase_server.myauth.models import UserType, get_typed_user_model from etebase_server.myauth.models import UserType, get_typed_user_model
from ..exceptions import AuthenticationFailed, transform_validation_error, HttpError
from ..msgpack import MsgpackRoute
from ..utils import BaseModel, permission_responses, msgpack_encode, msgpack_decode, get_user_username_email_kwargs
from ..dependencies import AuthData, get_auth_data, get_authenticated_user from ..dependencies import AuthData, get_auth_data, get_authenticated_user
from ..exceptions import AuthenticationFailed, HttpError, transform_validation_error
from ..msgpack import MsgpackResponse, MsgpackRoute
from ..utils import BaseModel, get_user_username_email_kwargs, msgpack_decode, msgpack_encode, permission_responses
User = get_typed_user_model() User = get_typed_user_model()
authentication_router = APIRouter(route_class=MsgpackRoute) authentication_router = APIRouter(route_class=MsgpackRoute)
@ -75,7 +76,7 @@ class LoginOut(BaseModel):
class Authentication(BaseModel): class Authentication(BaseModel):
class Config: class Config:
keep_untouched = (cached_property,) ignored_types = (cached_property,)
response: bytes response: bytes
signature: bytes signature: bytes
@ -187,7 +188,7 @@ def login_challenge(user: UserType = Depends(get_login_user)):
"userId": user.id, "userId": user.id,
} }
challenge = bytes(box.encrypt(msgpack_encode(challenge_data), encoder=nacl.encoding.RawEncoder)) challenge = bytes(box.encrypt(msgpack_encode(challenge_data), encoder=nacl.encoding.RawEncoder))
return LoginChallengeOut(salt=salt, challenge=challenge, version=user.userinfo.version) return MsgpackResponse(LoginChallengeOut(salt=salt, challenge=challenge, version=user.userinfo.version))
@authentication_router.post("/login/", response_model=LoginOut) @authentication_router.post("/login/", response_model=LoginOut)
@ -197,7 +198,7 @@ def login(data: Login, request: Request):
validate_login_request(data.response_data, data, user, "login", host) validate_login_request(data.response_data, data, user, "login", host)
ret = LoginOut.from_orm(user) ret = LoginOut.from_orm(user)
user_logged_in.send(sender=user.__class__, request=None, user=user) user_logged_in.send(sender=user.__class__, request=None, user=user)
return ret return MsgpackResponse(ret)
@authentication_router.post("/logout/", status_code=status.HTTP_204_NO_CONTENT, responses=permission_responses) @authentication_router.post("/logout/", status_code=status.HTTP_204_NO_CONTENT, responses=permission_responses)
@ -222,7 +223,7 @@ def dashboard_url(request: Request, user: UserType = Depends(get_authenticated_u
ret = { ret = {
"url": get_dashboard_url(CallbackContext(request.path_params, user=user)), "url": get_dashboard_url(CallbackContext(request.path_params, user=user)),
} }
return ret return MsgpackResponse(ret)
def signup_save(data: SignupIn, request: Request) -> UserType: def signup_save(data: SignupIn, request: Request) -> UserType:
@ -260,4 +261,4 @@ def signup(data: SignupIn, request: Request):
user = signup_save(data, request) user = signup_save(data, request)
ret = LoginOut.from_orm(user) ret = LoginOut.from_orm(user)
user_signed_up.send(sender=user.__class__, request=None, user=user) user_signed_up.send(sender=user.__class__, request=None, user=user)
return ret return MsgpackResponse(ret)

View File

@ -3,33 +3,34 @@ import typing as t
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from django.core import exceptions as django_exceptions from django.core import exceptions as django_exceptions
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.db import transaction, IntegrityError from django.db import IntegrityError, transaction
from django.db.models import Q, QuerySet from django.db.models import Q, QuerySet
from fastapi import APIRouter, Depends, status, Request, BackgroundTasks from fastapi import APIRouter, BackgroundTasks, Depends, Request, status
from etebase_server.django import models from etebase_server.django import models
from etebase_server.myauth.models import UserType from etebase_server.myauth.models import UserType
from .authentication import get_authenticated_user
from .websocket import get_ticket, TicketRequest, TicketOut from ..db_hack import django_db_cleanup_decorator
from ..exceptions import HttpError, transform_validation_error, PermissionDenied, ValidationError from ..dependencies import get_collection, get_collection_queryset, get_item_queryset
from ..msgpack import MsgpackRoute from ..exceptions import HttpError, PermissionDenied, ValidationError, transform_validation_error
from ..stoken_handler import filter_by_stoken_and_limit, filter_by_stoken, get_stoken_obj, get_queryset_stoken from ..msgpack import MsgpackRequest, MsgpackResponse, MsgpackRoute
from ..redis import redisw
from ..sendfile import sendfile
from ..stoken_handler import filter_by_stoken, filter_by_stoken_and_limit, get_queryset_stoken, get_stoken_obj
from ..utils import ( from ..utils import (
get_object_or_404, PERMISSIONS_READ,
PERMISSIONS_READWRITE,
BaseModel,
Context, Context,
Prefetch, Prefetch,
PrefetchQuery, PrefetchQuery,
get_object_or_404,
is_collection_admin, is_collection_admin,
msgpack_encode, msgpack_encode,
BaseModel,
permission_responses, permission_responses,
PERMISSIONS_READ,
PERMISSIONS_READWRITE,
) )
from ..dependencies import get_collection_queryset, get_item_queryset, get_collection from .authentication import get_authenticated_user
from ..sendfile import sendfile from .websocket import TicketOut, TicketRequest, get_ticket
from ..redis import redisw
from ..db_hack import django_db_cleanup_decorator
collection_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses) collection_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses)
item_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses) item_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses)
@ -51,7 +52,7 @@ class CollectionItemRevisionInOut(BaseModel):
chunks: t.List[ChunkType] chunks: t.List[ChunkType]
class Config: class Config:
orm_mode = True from_attributes = True
@classmethod @classmethod
def from_orm_context( def from_orm_context(
@ -71,13 +72,13 @@ class CollectionItemRevisionInOut(BaseModel):
class CollectionItemCommon(BaseModel): class CollectionItemCommon(BaseModel):
uid: str uid: str
version: int version: int
encryptionKey: t.Optional[bytes] encryptionKey: t.Optional[bytes] = None
content: CollectionItemRevisionInOut content: CollectionItemRevisionInOut
class CollectionItemOut(CollectionItemCommon): class CollectionItemOut(CollectionItemCommon):
class Config: class Config:
orm_mode = True from_attributes = True
@classmethod @classmethod
def from_orm_context( def from_orm_context(
@ -92,12 +93,12 @@ class CollectionItemOut(CollectionItemCommon):
class CollectionItemIn(CollectionItemCommon): class CollectionItemIn(CollectionItemCommon):
etag: t.Optional[str] etag: t.Optional[str] = None
class CollectionCommon(BaseModel): class CollectionCommon(BaseModel):
# FIXME: remove optional once we finish collection-type-migration # FIXME: remove optional once we finish collection-type-migration
collectionType: t.Optional[bytes] collectionType: t.Optional[bytes] = None
collectionKey: bytes collectionKey: bytes
@ -131,27 +132,27 @@ class RemovedMembershipOut(BaseModel):
class CollectionListResponse(BaseModel): class CollectionListResponse(BaseModel):
data: t.List[CollectionOut] data: t.List[CollectionOut]
stoken: t.Optional[str] stoken: t.Optional[str] = None
done: bool done: bool
removedMemberships: t.Optional[t.List[RemovedMembershipOut]] removedMemberships: t.Optional[t.List[RemovedMembershipOut]] = None
class CollectionItemListResponse(BaseModel): class CollectionItemListResponse(BaseModel):
data: t.List[CollectionItemOut] data: t.List[CollectionItemOut]
stoken: t.Optional[str] stoken: t.Optional[str] = None
done: bool done: bool
class CollectionItemRevisionListResponse(BaseModel): class CollectionItemRevisionListResponse(BaseModel):
data: t.List[CollectionItemRevisionInOut] data: t.List[CollectionItemRevisionInOut]
iterator: t.Optional[str] iterator: t.Optional[str] = None
done: bool done: bool
class CollectionItemBulkGetIn(BaseModel): class CollectionItemBulkGetIn(BaseModel):
uid: str uid: str
etag: t.Optional[str] etag: t.Optional[str] = None
class ItemDepIn(BaseModel): class ItemDepIn(BaseModel):
@ -172,7 +173,7 @@ class ItemDepIn(BaseModel):
class ItemBatchIn(BaseModel): class ItemBatchIn(BaseModel):
items: t.List[CollectionItemIn] items: t.List[CollectionItemIn]
deps: t.Optional[t.List[ItemDepIn]] deps: t.Optional[t.List[ItemDepIn]] = None
def validate_db(self): def validate_db(self):
if self.deps is not None: if self.deps is not None:
@ -208,7 +209,7 @@ def collection_list_common(
prefetch: Prefetch, prefetch: Prefetch,
) -> CollectionListResponse: ) -> CollectionListResponse:
result, new_stoken_obj, done = filter_by_stoken_and_limit( result, new_stoken_obj, done = filter_by_stoken_and_limit(
stoken, limit, queryset, models.Collection.stoken_annotation stoken, limit, queryset.filter(items__revisions__current=True), models.Collection.stoken_annotation
) )
new_stoken = new_stoken_obj and new_stoken_obj.uid new_stoken = new_stoken_obj and new_stoken_obj.uid
context = Context(user, prefetch) context = Context(user, prefetch)
@ -274,7 +275,7 @@ def list_multi(
Q(members__collectionType__uid__in=data.collectionTypes) | Q(members__collectionType__isnull=True) Q(members__collectionType__uid__in=data.collectionTypes) | Q(members__collectionType__isnull=True)
) )
return collection_list_common(queryset, user, stoken, limit, prefetch) return MsgpackResponse(collection_list_common(queryset, user, stoken, limit, prefetch))
@collection_router.get("/", response_model=CollectionListResponse, dependencies=PERMISSIONS_READ) @collection_router.get("/", response_model=CollectionListResponse, dependencies=PERMISSIONS_READ)
@ -285,7 +286,7 @@ def collection_list(
user: UserType = Depends(get_authenticated_user), user: UserType = Depends(get_authenticated_user),
queryset: CollectionQuerySet = Depends(get_collection_queryset), queryset: CollectionQuerySet = Depends(get_collection_queryset),
): ):
return collection_list_common(queryset, user, stoken, limit, prefetch) return MsgpackResponse(collection_list_common(queryset, user, stoken, limit, prefetch))
def process_revisions_for_item(item: models.CollectionItem, revision_data: CollectionItemRevisionInOut): def process_revisions_for_item(item: models.CollectionItem, revision_data: CollectionItemRevisionInOut):
@ -341,7 +342,10 @@ def _create(data: CollectionIn, user: UserType):
# TODO # TODO
process_revisions_for_item(main_item, data.item.content) process_revisions_for_item(main_item, data.item.content)
collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=data.collectionType, owner=user) try:
collection_type_obj, _ = models.CollectionType.objects.get_or_create(uid=data.collectionType, owner=user)
except IntegrityError:
raise ValidationError("bad_collection_type", "collectionType is null")
models.CollectionMember( models.CollectionMember(
collection=instance, collection=instance,
@ -364,7 +368,7 @@ def collection_get(
user: UserType = Depends(get_authenticated_user), user: UserType = Depends(get_authenticated_user),
prefetch: Prefetch = PrefetchQuery, prefetch: Prefetch = PrefetchQuery,
): ):
return CollectionOut.from_orm_context(obj, Context(user, prefetch)) return MsgpackResponse(CollectionOut.from_orm_context(obj, Context(user, prefetch)))
def item_create(item_model: CollectionItemIn, collection: models.Collection, validate_etag: bool): def item_create(item_model: CollectionItemIn, collection: models.Collection, validate_etag: bool):
@ -373,7 +377,7 @@ def item_create(item_model: CollectionItemIn, collection: models.Collection, val
revision_data = item_model.content revision_data = item_model.content
uid = item_model.uid uid = item_model.uid
Model = models.CollectionItem Model = models.CollectionItem # noqa: N806
with transaction.atomic(): with transaction.atomic():
instance, created = Model.objects.get_or_create( instance, created = Model.objects.get_or_create(
@ -396,7 +400,7 @@ def item_create(item_model: CollectionItemIn, collection: models.Collection, val
if not created: if not created:
# We don't have to use select_for_update here because the unique constraint on current guards against # We don't have to use select_for_update here because the unique constraint on current guards against
# the race condition. But it's a good idea because it'll lock and wait rather than fail. # the race condition. But it's a good idea because it'll lock and wait rather than fail.
current_revision = instance.revisions.filter(current=True).select_for_update().first() current_revision = instance.revisions.filter(current=True).select_for_update()[0]
assert current_revision is not None assert current_revision is not None
current_revision.current = None current_revision.current = None
current_revision.save() current_revision.save()
@ -417,7 +421,7 @@ def item_get(
prefetch: Prefetch = PrefetchQuery, prefetch: Prefetch = PrefetchQuery,
): ):
obj = queryset.get(uid=item_uid) obj = queryset.get(uid=item_uid)
return CollectionItemOut.from_orm_context(obj, Context(user, prefetch)) return MsgpackResponse(CollectionItemOut.from_orm_context(obj, Context(user, prefetch)))
def item_list_common( def item_list_common(
@ -428,7 +432,7 @@ def item_list_common(
prefetch: Prefetch, prefetch: Prefetch,
) -> CollectionItemListResponse: ) -> CollectionItemListResponse:
result, new_stoken_obj, done = filter_by_stoken_and_limit( result, new_stoken_obj, done = filter_by_stoken_and_limit(
stoken, limit, queryset, models.CollectionItem.stoken_annotation stoken, limit, queryset.filter(revisions__current=True), models.CollectionItem.stoken_annotation
) )
new_stoken = new_stoken_obj and new_stoken_obj.uid new_stoken = new_stoken_obj and new_stoken_obj.uid
context = Context(user, prefetch) context = Context(user, prefetch)
@ -449,7 +453,7 @@ def item_list(
queryset = queryset.filter(parent__isnull=True) queryset = queryset.filter(parent__isnull=True)
response = item_list_common(queryset, user, stoken, limit, prefetch) response = item_list_common(queryset, user, stoken, limit, prefetch)
return response return MsgpackResponse(response)
@item_router.post("/item/subscription-ticket/", response_model=TicketOut, dependencies=PERMISSIONS_READ) @item_router.post("/item/subscription-ticket/", response_model=TicketOut, dependencies=PERMISSIONS_READ)
@ -458,7 +462,7 @@ async def item_list_subscription_ticket(
user: UserType = Depends(get_authenticated_user), user: UserType = Depends(get_authenticated_user),
): ):
"""Get an authentication ticket that can be used with the websocket endpoint""" """Get an authentication ticket that can be used with the websocket endpoint"""
return await get_ticket(TicketRequest(collection=collection.uid), user) return MsgpackResponse(await get_ticket(TicketRequest(collection=collection.uid), user))
def item_bulk_common( def item_bulk_common(
@ -526,10 +530,12 @@ def item_revisions(
ret_data = [CollectionItemRevisionInOut.from_orm_context(revision, context) for revision in result] ret_data = [CollectionItemRevisionInOut.from_orm_context(revision, context) for revision in result]
iterator = ret_data[-1].uid if len(result) > 0 else None iterator = ret_data[-1].uid if len(result) > 0 else None
return CollectionItemRevisionListResponse( return MsgpackResponse(
data=ret_data, CollectionItemRevisionListResponse(
iterator=iterator, data=ret_data,
done=done, iterator=iterator,
done=done,
)
) )
@ -559,10 +565,12 @@ def fetch_updates(
new_stoken = new_stoken or stoken_rev_uid new_stoken = new_stoken or stoken_rev_uid
context = Context(user, prefetch) context = Context(user, prefetch)
return CollectionItemListResponse( return MsgpackResponse(
data=[CollectionItemOut.from_orm_context(item, context) for item in queryset], CollectionItemListResponse(
stoken=new_stoken, data=[CollectionItemOut.from_orm_context(item, context) for item in queryset],
done=True, # we always return all the items, so it's always done stoken=new_stoken,
done=True, # we always return all the items, so it's always done
)
) )
@ -574,7 +582,9 @@ def item_transaction(
stoken: t.Optional[str] = None, stoken: t.Optional[str] = None,
user: UserType = Depends(get_authenticated_user), user: UserType = Depends(get_authenticated_user),
): ):
return item_bulk_common(data, user, stoken, collection_uid, validate_etag=True, background_tasks=background_tasks) return MsgpackResponse(
item_bulk_common(data, user, stoken, collection_uid, validate_etag=True, background_tasks=background_tasks)
)
@item_router.post("/item/batch/", dependencies=[Depends(has_write_access), *PERMISSIONS_READWRITE]) @item_router.post("/item/batch/", dependencies=[Depends(has_write_access), *PERMISSIONS_READWRITE])
@ -585,7 +595,9 @@ def item_batch(
stoken: t.Optional[str] = None, stoken: t.Optional[str] = None,
user: UserType = Depends(get_authenticated_user), user: UserType = Depends(get_authenticated_user),
): ):
return item_bulk_common(data, user, stoken, collection_uid, validate_etag=False, background_tasks=background_tasks) return MsgpackResponse(
item_bulk_common(data, user, stoken, collection_uid, validate_etag=False, background_tasks=background_tasks)
)
# Chunks # Chunks
@ -610,7 +622,12 @@ async def chunk_update(
collection: models.Collection = Depends(get_collection), collection: models.Collection = Depends(get_collection),
): ):
# IGNORED FOR NOW: col_it = get_object_or_404(col.items, uid=collection_item_uid) # IGNORED FOR NOW: col_it = get_object_or_404(col.items, uid=collection_item_uid)
content_file = ContentFile(await request.body()) if isinstance(request, MsgpackRequest):
body = await request.raw_body()
else:
body = await request.body()
content_file = ContentFile(body)
try: try:
await chunk_save(chunk_uid, collection, content_file) await chunk_save(chunk_uid, collection, content_file)
except IntegrityError: except IntegrityError:

View File

@ -1,26 +1,27 @@
import typing as t import typing as t
from django.db import transaction, IntegrityError from django.db import IntegrityError, transaction
from django.db.models import QuerySet from django.db.models import QuerySet
from fastapi import APIRouter, Depends, status, Request from fastapi import APIRouter, Depends, Request, status
from etebase_server.django import models from etebase_server.django import models
from etebase_server.django.utils import get_user_queryset, CallbackContext from etebase_server.django.utils import CallbackContext, get_user_queryset
from etebase_server.myauth.models import UserType, get_typed_user_model from etebase_server.myauth.models import UserType, get_typed_user_model
from .authentication import get_authenticated_user
from ..db_hack import django_db_cleanup_decorator
from ..exceptions import HttpError, PermissionDenied from ..exceptions import HttpError, PermissionDenied
from ..msgpack import MsgpackRoute from ..msgpack import MsgpackResponse, MsgpackRoute
from ..utils import ( from ..utils import (
get_object_or_404,
get_user_username_email_kwargs,
Context,
is_collection_admin,
BaseModel,
permission_responses,
PERMISSIONS_READ, PERMISSIONS_READ,
PERMISSIONS_READWRITE, PERMISSIONS_READWRITE,
BaseModel,
Context,
get_object_or_404,
get_user_username_email_kwargs,
is_collection_admin,
permission_responses,
) )
from ..db_hack import django_db_cleanup_decorator from .authentication import get_authenticated_user
User = get_typed_user_model() User = get_typed_user_model()
invitation_incoming_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses) invitation_incoming_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses)
@ -33,7 +34,7 @@ class UserInfoOut(BaseModel):
pubkey: bytes pubkey: bytes
class Config: class Config:
orm_mode = True from_attributes = True
@classmethod @classmethod
def from_orm(cls: t.Type["UserInfoOut"], obj: models.UserInfo) -> "UserInfoOut": def from_orm(cls: t.Type["UserInfoOut"], obj: models.UserInfo) -> "UserInfoOut":
@ -66,7 +67,7 @@ class CollectionInvitationOut(CollectionInvitationCommon):
fromPubkey: bytes fromPubkey: bytes
class Config: class Config:
orm_mode = True from_attributes = True
@classmethod @classmethod
def from_orm(cls: t.Type["CollectionInvitationOut"], obj: models.CollectionInvitation) -> "CollectionInvitationOut": def from_orm(cls: t.Type["CollectionInvitationOut"], obj: models.CollectionInvitation) -> "CollectionInvitationOut":
@ -84,7 +85,7 @@ class CollectionInvitationOut(CollectionInvitationCommon):
class InvitationListResponse(BaseModel): class InvitationListResponse(BaseModel):
data: t.List[CollectionInvitationOut] data: t.List[CollectionInvitationOut]
iterator: t.Optional[str] iterator: t.Optional[str] = None
done: bool done: bool
@ -120,7 +121,7 @@ def list_common(
iterator = ret_data[-1].uid if len(result) > 0 else None iterator = ret_data[-1].uid if len(result) > 0 else None
return InvitationListResponse( return InvitationListResponse(
data=ret_data, data=[CollectionInvitationOut.from_orm(x) for x in ret_data],
iterator=iterator, iterator=iterator,
done=done, done=done,
) )
@ -132,7 +133,7 @@ def incoming_list(
limit: int = 50, limit: int = 50,
queryset: InvitationQuerySet = Depends(get_incoming_queryset), queryset: InvitationQuerySet = Depends(get_incoming_queryset),
): ):
return list_common(queryset, iterator, limit) return MsgpackResponse(list_common(queryset, iterator, limit))
@invitation_incoming_router.get( @invitation_incoming_router.get(
@ -143,7 +144,7 @@ def incoming_get(
queryset: InvitationQuerySet = Depends(get_incoming_queryset), queryset: InvitationQuerySet = Depends(get_incoming_queryset),
): ):
obj = get_object_or_404(queryset, uid=invitation_uid) obj = get_object_or_404(queryset, uid=invitation_uid)
return CollectionInvitationOut.from_orm(obj) return MsgpackResponse(CollectionInvitationOut.from_orm(obj))
@invitation_incoming_router.delete( @invitation_incoming_router.delete(
@ -218,7 +219,7 @@ def outgoing_list(
limit: int = 50, limit: int = 50,
queryset: InvitationQuerySet = Depends(get_outgoing_queryset), queryset: InvitationQuerySet = Depends(get_outgoing_queryset),
): ):
return list_common(queryset, iterator, limit) return MsgpackResponse(list_common(queryset, iterator, limit))
@invitation_outgoing_router.delete( @invitation_outgoing_router.delete(
@ -241,4 +242,4 @@ def outgoing_fetch_user_profile(
kwargs = get_user_username_email_kwargs(username) kwargs = get_user_username_email_kwargs(username)
user = get_object_or_404(get_user_queryset(User.objects.all(), CallbackContext(request.path_params)), **kwargs) user = get_object_or_404(get_user_queryset(User.objects.all(), CallbackContext(request.path_params)), **kwargs)
user_info = get_object_or_404(models.UserInfo.objects.all(), owner=user) user_info = get_object_or_404(models.UserInfo.objects.all(), owner=user)
return UserInfoOut.from_orm(user_info) return MsgpackResponse(UserInfoOut.from_orm(user_info))

View File

@ -6,12 +6,12 @@ from fastapi import APIRouter, Depends, status
from etebase_server.django import models from etebase_server.django import models
from etebase_server.myauth.models import UserType, get_typed_user_model from etebase_server.myauth.models import UserType, get_typed_user_model
from .authentication import get_authenticated_user
from ..msgpack import MsgpackRoute
from ..utils import get_object_or_404, BaseModel, permission_responses, PERMISSIONS_READ, PERMISSIONS_READWRITE
from ..stoken_handler import filter_by_stoken_and_limit
from ..db_hack import django_db_cleanup_decorator
from ..db_hack import django_db_cleanup_decorator
from ..msgpack import MsgpackResponse, MsgpackRoute
from ..stoken_handler import filter_by_stoken_and_limit
from ..utils import PERMISSIONS_READ, PERMISSIONS_READWRITE, BaseModel, get_object_or_404, permission_responses
from .authentication import get_authenticated_user
from .collection import get_collection, verify_collection_admin from .collection import get_collection, verify_collection_admin
User = get_typed_user_model() User = get_typed_user_model()
@ -39,7 +39,7 @@ class CollectionMemberOut(BaseModel):
accessLevel: models.AccessLevels accessLevel: models.AccessLevels
class Config: class Config:
orm_mode = True from_attributes = True
@classmethod @classmethod
def from_orm(cls: t.Type["CollectionMemberOut"], obj: models.CollectionMember) -> "CollectionMemberOut": def from_orm(cls: t.Type["CollectionMemberOut"], obj: models.CollectionMember) -> "CollectionMemberOut":
@ -48,7 +48,7 @@ class CollectionMemberOut(BaseModel):
class MemberListResponse(BaseModel): class MemberListResponse(BaseModel):
data: t.List[CollectionMemberOut] data: t.List[CollectionMemberOut]
iterator: t.Optional[str] iterator: t.Optional[str] = None
done: bool done: bool
@ -66,10 +66,12 @@ def member_list(
) )
new_stoken = new_stoken_obj and new_stoken_obj.uid new_stoken = new_stoken_obj and new_stoken_obj.uid
return MemberListResponse( return MsgpackResponse(
data=[CollectionMemberOut.from_orm(item) for item in result], MemberListResponse(
iterator=new_stoken, data=[CollectionMemberOut.from_orm(item) for item in result],
done=done, iterator=new_stoken,
done=done,
)
) )

View File

@ -3,12 +3,13 @@ from django.db import transaction
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from fastapi import APIRouter, Request, status from fastapi import APIRouter, Request, status
from etebase_server.django.utils import get_user_queryset, CallbackContext from etebase_server.django.utils import CallbackContext, get_user_queryset
from .authentication import SignupIn, signup_save
from ..msgpack import MsgpackRoute
from ..exceptions import HttpError
from etebase_server.myauth.models import get_typed_user_model from etebase_server.myauth.models import get_typed_user_model
from ..exceptions import HttpError
from ..msgpack import MsgpackRoute
from .authentication import SignupIn, signup_save
test_reset_view_router = APIRouter(route_class=MsgpackRoute, tags=["test helpers"]) test_reset_view_router = APIRouter(route_class=MsgpackRoute, tags=["test helpers"])
User = get_typed_user_model() User = get_typed_user_model()

View File

@ -1,12 +1,13 @@
import asyncio import asyncio
import typing as t import typing as t
import aioredis import nacl.encoding
import nacl.utils
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from django.db.models import QuerySet from django.db.models import QuerySet
from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, status from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect, status
import nacl.encoding from redis import asyncio as aioredis
import nacl.utils from redis.exceptions import ConnectionError
from etebase_server.django import models from etebase_server.django import models
from etebase_server.django.utils import CallbackContext, get_user_queryset from etebase_server.django.utils import CallbackContext, get_user_queryset
@ -18,7 +19,6 @@ from ..msgpack import MsgpackRoute, msgpack_decode, msgpack_encode
from ..redis import redisw from ..redis import redisw
from ..utils import BaseModel, permission_responses from ..utils import BaseModel, permission_responses
User = get_typed_user_model() User = get_typed_user_model()
websocket_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses) websocket_router = APIRouter(route_class=MsgpackRoute, responses=permission_responses)
CollectionQuerySet = QuerySet[models.Collection] CollectionQuerySet = QuerySet[models.Collection]
@ -51,7 +51,7 @@ async def get_ticket(
uid = nacl.encoding.URLSafeBase64Encoder.encode(nacl.utils.random(32)) uid = nacl.encoding.URLSafeBase64Encoder.encode(nacl.utils.random(32))
ticket_model = TicketInner(user=user.id, req=ticket_request) ticket_model = TicketInner(user=user.id, req=ticket_request)
ticket_raw = msgpack_encode(ticket_model.dict()) ticket_raw = msgpack_encode(ticket_model.dict())
await redisw.redis.set(uid, ticket_raw, expire=TICKET_VALIDITY_SECONDS * 1000) await redisw.redis.set(uid, ticket_raw, ex=TICKET_VALIDITY_SECONDS * 1000)
return TicketOut(ticket=uid) return TicketOut(ticket=uid)
@ -103,9 +103,9 @@ async def send_item_updates(
async def redis_connector(websocket: WebSocket, ticket_model: TicketInner, user: UserType, stoken: t.Optional[str]): async def redis_connector(websocket: WebSocket, ticket_model: TicketInner, user: UserType, stoken: t.Optional[str]):
async def producer_handler(r: aioredis.Redis, ws: WebSocket): async def producer_handler(r: aioredis.Redis, ws: WebSocket):
pubsub = r.pubsub()
channel_name = f"col.{ticket_model.req.collection}" channel_name = f"col.{ticket_model.req.collection}"
(channel,) = await r.psubscribe(channel_name) await pubsub.subscribe(channel_name)
assert isinstance(channel, aioredis.Channel)
# Send missing items if we are not up to date # Send missing items if we are not up to date
queryset: QuerySet[models.Collection] = get_collection_queryset(user) queryset: QuerySet[models.Collection] = get_collection_queryset(user)
@ -117,26 +117,29 @@ async def redis_connector(websocket: WebSocket, ticket_model: TicketInner, user:
return return
await send_item_updates(websocket, collection, user, stoken) await send_item_updates(websocket, collection, user, stoken)
async def handle_message():
msg = await pubsub.get_message(ignore_subscribe_messages=True, timeout=20)
message_raw = t.cast(t.Optional[t.Tuple[str, bytes]], msg)
if message_raw:
_, message = message_raw
await ws.send_bytes(message)
try: try:
while True: while True:
# We wait on the websocket so we fail if web sockets fail or get data # We wait on the websocket so we fail if web sockets fail or get data
receive = asyncio.create_task(websocket.receive()) receive = asyncio.create_task(websocket.receive())
done, pending = await asyncio.wait( done, pending = await asyncio.wait(
{receive, channel.wait_message()}, return_when=asyncio.FIRST_COMPLETED {receive, handle_message()},
return_when=asyncio.FIRST_COMPLETED,
) )
for task in pending: for task in pending:
task.cancel() task.cancel()
if receive in done: if receive in done:
# Web socket should never receieve any data # Web socket should never receive any data
await websocket.close(code=status.WS_1008_POLICY_VIOLATION) await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
return return
message_raw = t.cast(t.Optional[t.Tuple[str, bytes]], await channel.get()) except ConnectionError:
if message_raw:
_, message = message_raw
await ws.send_bytes(message)
except aioredis.errors.ConnectionClosedError:
await websocket.close(code=status.WS_1012_SERVICE_RESTART) await websocket.close(code=status.WS_1012_SERVICE_RESTART)
except WebSocketDisconnect: except WebSocketDisconnect:
pass pass

View File

@ -1,14 +1,15 @@
import logging
import os
from functools import lru_cache from functools import lru_cache
from importlib import import_module from importlib import import_module
from pathlib import Path, PurePath from pathlib import Path, PurePath
from urllib.parse import quote from urllib.parse import quote
import logging
from fastapi import status
from ..exceptions import HttpError
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from fastapi import status
from ..exceptions import HttpError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -32,9 +33,7 @@ def _convert_file_to_url(path):
path_obj = PurePath(path) path_obj = PurePath(path)
relpath = path_obj.relative_to(path_root) relpath = path_obj.relative_to(path_root)
# Python 3.5: Path.resolve() has no `strict` kwarg, so use pathmod from an url = os.path.normpath(str(url_root / relpath))
# already instantiated Path object
url = relpath._flavour.pathmod.normpath(str(url_root / relpath))
return quote(str(url)) return quote(str(url))
@ -48,9 +47,7 @@ def _sanitize_path(filepath):
filepath_obj = Path(filepath) filepath_obj = Path(filepath)
# get absolute path # get absolute path
# Python 3.5: Path.resolve() has no `strict` kwarg, so use pathmod from an filepath_abs = Path(os.path.normpath(str(path_root / filepath_obj)))
# already instantiated Path object
filepath_abs = Path(filepath_obj._flavour.pathmod.normpath(str(path_root / filepath_obj)))
# if filepath_abs is not relative to path_root, relative_to throws an error # if filepath_abs is not relative to path_root, relative_to throws an error
try: try:

View File

@ -47,7 +47,6 @@ def get_queryset_stoken(queryset: t.Iterable[t.Any]) -> t.Optional[Stoken]:
def filter_by_stoken_and_limit( def filter_by_stoken_and_limit(
stoken: t.Optional[str], limit: int, queryset: QuerySet, stoken_annotation: StokenAnnotation stoken: t.Optional[str], limit: int, queryset: QuerySet, stoken_annotation: StokenAnnotation
) -> t.Tuple[list, t.Optional[Stoken], bool]: ) -> t.Tuple[list, t.Optional[Stoken], bool]:
queryset, stoken_rev = filter_by_stoken(stoken=stoken, queryset=queryset, stoken_annotation=stoken_annotation) queryset, stoken_rev = filter_by_stoken(stoken=stoken, queryset=queryset, stoken_annotation=stoken_annotation)
result = list(queryset[: limit + 1]) result = list(queryset[: limit + 1])

View File

@ -1,14 +1,13 @@
import base64
import dataclasses import dataclasses
import typing as t import typing as t
from typing_extensions import Literal
import msgpack import msgpack
import base64
from fastapi import status, Query, Depends
from pydantic import BaseModel as PyBaseModel
from django.db.models import Model, QuerySet
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Model, QuerySet
from fastapi import Depends, Query, status
from pydantic import BaseModel as PyBaseModel
from typing_extensions import Literal
from etebase_server.django import app_settings from etebase_server.django import app_settings
from etebase_server.django.models import AccessLevels from etebase_server.django.models import AccessLevels
@ -26,10 +25,7 @@ T = t.TypeVar("T", bound=Model, covariant=True)
class BaseModel(PyBaseModel): class BaseModel(PyBaseModel):
class Config: pass
json_encoders = {
bytes: lambda x: x,
}
@dataclasses.dataclass @dataclasses.dataclass

View File

@ -1,12 +1,21 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin
from .models import User
from .forms import AdminUserCreationForm from .forms import AdminUserCreationForm
from .models import User
class UserAdmin(DjangoUserAdmin): class UserAdmin(DjangoUserAdmin):
add_form = AdminUserCreationForm add_form = AdminUserCreationForm
add_fieldsets = ((None, {"classes": ("wide",), "fields": ("username",),}),) add_fieldsets = (
(
None,
{
"classes": ("wide",),
"fields": ("username",),
},
),
)
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)

View File

@ -1,5 +1,6 @@
from django import forms from django import forms
from django.contrib.auth.forms import UsernameField from django.contrib.auth.forms import UsernameField
from etebase_server.myauth.models import get_typed_user_model from etebase_server.myauth.models import get_typed_user_model
User = get_typed_user_model() User = get_typed_user_model()

View File

@ -1,15 +1,15 @@
import logging import logging
from django.utils import timezone import ldap
from django.conf import settings from django.conf import settings
from django.core.exceptions import PermissionDenied as DjangoPermissionDenied from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
from etebase_server.django.utils import CallbackContext from django.utils import timezone
from etebase_server.myauth.models import get_typed_user_model, UserType
from etebase_server.fastapi.dependencies import get_authenticated_user
from etebase_server.fastapi.exceptions import PermissionDenied as FastAPIPermissionDenied
from fastapi import Depends from fastapi import Depends
import ldap from etebase_server.django.utils import CallbackContext
from etebase_server.fastapi.dependencies import get_authenticated_user
from etebase_server.fastapi.exceptions import PermissionDenied as FastAPIPermissionDenied
from etebase_server.myauth.models import UserType, get_typed_user_model
User = get_typed_user_model() User = get_typed_user_model()
@ -54,7 +54,7 @@ class LDAPConnection:
try: try:
self.__ldap_connection.simple_bind_s(ldap_setting("BIND_DN", ""), password) self.__ldap_connection.simple_bind_s(ldap_setting("BIND_DN", ""), password)
except ldap.LDAPError as err: except ldap.LDAPError as err:
logging.error(f"LDAP Error occuring during bind: {err.desc}") logging.error(f"LDAP Error occurring during bind: {err.desc}")
def __is_cache_valid(self, username): def __is_cache_valid(self, username):
"""Returns True if the cache entry is still valid. Returns False otherwise.""" """Returns True if the cache entry is still valid. Returns False otherwise."""
@ -85,7 +85,7 @@ class LDAPConnection:
# we may expect ldap.NO_RESULTS_RETURNED, but not any other error # we may expect ldap.NO_RESULTS_RETURNED, but not any other error
return False return False
except ldap.LDAPError as err: except ldap.LDAPError as err:
logging.error(f"Error occured while performing an LDAP query: {err.desc}") logging.error(f"Error occurred while performing an LDAP query: {err.desc}")
return False return False
if len(result) == 1: if len(result) == 1:

View File

@ -1,21 +1,13 @@
import typing as t import typing as t
from django.contrib.auth.models import AbstractUser, UserManager as DjangoUserManager from django.contrib.auth.models import AbstractUser, UserManager as DjangoUserManager
from django.core import validators from django.contrib.auth.validators import UnicodeUsernameValidator
from django.db import models from django.db import models
from django.utils.deconstruct import deconstructible
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@deconstructible
class UnicodeUsernameValidator(validators.RegexValidator):
regex = r"^[\w.-]+\Z"
message = _("Enter a valid username. This value may contain only letters, " "numbers, and ./-/_ characters.")
flags = 0
class UserManager(DjangoUserManager): class UserManager(DjangoUserManager):
def get_by_natural_key(self, username: str): def get_by_natural_key(self, username: t.Optional[str]):
return self.get(**{self.model.USERNAME_FIELD + "__iexact": username}) return self.get(**{self.model.USERNAME_FIELD + "__iexact": username})

View File

@ -1,3 +1 @@
from django.test import TestCase
# Create your tests here. # Create your tests here.

View File

@ -1,3 +1 @@
from django.shortcuts import render
# Create your views here. # Create your views here.

View File

@ -10,8 +10,9 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.0/ref/settings/ https://docs.djangoproject.com/en/3.0/ref/settings/
""" """
import os
import configparser import configparser
import os
from .utils import get_secret_from_file from .utils import get_secret_from_file
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
@ -20,6 +21,7 @@ BASE_DIR = os.path.dirname(SOURCE_DIR)
AUTH_USER_MODEL = "myauth.User" AUTH_USER_MODEL = "myauth.User"
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
@ -44,7 +46,7 @@ DATABASES = {
} }
} }
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Application definition # Application definition
@ -126,13 +128,12 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/ # https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATIC_URL = "/static/" STATIC_URL = "/static/"
STATIC_ROOT = os.environ.get("DJANGO_STATIC_ROOT", os.path.join(BASE_DIR, "static"))
MEDIA_ROOT = os.environ.get("DJANGO_MEDIA_ROOT", os.path.join(BASE_DIR, "media")) MEDIA_ROOT = os.path.join(BASE_DIR, "media")
MEDIA_URL = "/user-media/" MEDIA_URL = "/user-media/"
# Define where to find configuration files # Define where to find configuration files
config_locations = [ config_locations = [
os.environ.get("ETEBASE_EASY_CONFIG_PATH", ""), os.environ.get("ETEBASE_EASY_CONFIG_PATH", ""),
@ -163,6 +164,8 @@ if any(os.path.isfile(x) for x in config_locations):
if "allowed_hosts" in config: if "allowed_hosts" in config:
ALLOWED_HOSTS = [y for x, y in config.items("allowed_hosts")] ALLOWED_HOSTS = [y for x, y in config.items("allowed_hosts")]
CSRF_TRUSTED_ORIGINS = ["https://" + y for x, y in config.items("allowed_hosts")] + \
["http://" + y for x, y in config.items("allowed_hosts")]
if "database" in config: if "database" in config:
DATABASES = {"default": {x.upper(): y for x, y in config.items("database")}} DATABASES = {"default": {x.upper(): y for x, y in config.items("database")}}
@ -193,9 +196,15 @@ if any(os.path.isfile(x) for x in config_locations):
SENDFILE_BACKEND = "etebase_server.fastapi.sendfile.backends.simple" SENDFILE_BACKEND = "etebase_server.fastapi.sendfile.backends.simple"
SENDFILE_ROOT = MEDIA_ROOT SENDFILE_ROOT = MEDIA_ROOT
if "DJANGO_STATIC_ROOT" in os.environ:
STATIC_ROOT = os.environ["DJANGO_STATIC_ROOT"]
if "DJANGO_MEDIA_ROOT" in os.environ:
MEDIA_ROOT = os.environ["DJANGO_MEDIA_ROOT"]
# Make an `etebase_server_settings` module available to override settings. # Make an `etebase_server_settings` module available to override settings.
try: try:
from etebase_server_settings import * from etebase_server_settings import * # noqa: F403
except ImportError: except ImportError:
pass pass

View File

@ -1,11 +1,6 @@
import os
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.views.static import serve
from django.contrib.staticfiles import finders
urlpatterns = [ urlpatterns = [
path("admin/", admin.site.urls), path("admin/", admin.site.urls),

View File

@ -12,10 +12,11 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.core.management import utils
import os import os
import stat import stat
from django.core.management import utils
def get_secret_from_file(path): def get_secret_from_file(path):
try: try:

View File

@ -1,5 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Django's command-line utility for administrative tasks.""" """Django's command-line utility for administrative tasks."""
import os import os
import sys import sys

View File

@ -4,3 +4,25 @@ line-length = 120
[build-system] [build-system]
requires = ["setuptools>=42"] requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[tool.ruff]
line-length = 120
exclude = [
".git",
".git-rewrite",
".mypy_cache",
".pytype",
".ruff_cache",
".venv",
"build",
"dist",
"node_modules",
"migrations", # Alembic migrations
]
[tool.ruff.lint]
select = ["E", "F", "I", "N", "T20", "W"]
ignore = ["E203", "E501", "E711", "E712", "N803", "N815", "N818", "T201"]
[tool.ruff.lint.isort]
combine-as-imports = true

View File

@ -1,65 +1,53 @@
# #
# This file is autogenerated by pip-compile with python 3.10 # This file is autogenerated by pip-compile with Python 3.12
# To update, run: # by the following command:
# #
# pip-compile --output-file=requirements-dev.txt requirements.in/development.txt # pip-compile --output-file=requirements-dev.txt requirements.in/development.txt
# #
asgiref==3.5.0 asgiref==3.8.1
# via django
black==22.1.0
# via -r requirements.in/development.txt
click==8.0.4
# via # via
# black # django
# pip-tools
coverage==6.3.2
# via -r requirements.in/development.txt
django==3.2.12
# via
# -r requirements.in/development.txt
# django-stubs # django-stubs
# django-stubs-ext build==1.2.1
django-stubs==1.9.0
# via -r requirements.in/development.txt
django-stubs-ext==0.3.1
# via django-stubs
mypy==0.941
# via django-stubs
mypy-extensions==0.4.3
# via
# black
# mypy
pathspec==0.9.0
# via black
pep517==0.12.0
# via pip-tools # via pip-tools
pip-tools==6.5.1 click==8.1.7
# via pip-tools
coverage==7.5.3
# via -r requirements.in/development.txt # via -r requirements.in/development.txt
platformdirs==2.5.1 django==5.0.6
# via black
pytz==2022.1
# via django
pywatchman==1.4.1
# via -r requirements.in/development.txt
sqlparse==0.4.2
# via django
toml==0.10.2
# via django-stubs
tomli==2.0.1
# via # via
# black # django-stubs
# mypy # django-stubs-ext
# pep517 django-stubs==5.0.2
types-pytz==2021.3.6 # via -r requirements.in/development.txt
django-stubs-ext==5.0.2
# via django-stubs # via django-stubs
types-pyyaml==6.0.5 mypy==1.10.0
# via -r requirements.in/development.txt
mypy-extensions==1.0.0
# via mypy
packaging==24.0
# via build
pip-tools==7.4.1
# via -r requirements.in/development.txt
pyproject-hooks==1.1.0
# via
# build
# pip-tools
pywatchman==2.0.0
# via -r requirements.in/development.txt
ruff==0.4.8
# via -r requirements.in/development.txt
sqlparse==0.5.0
# via django
types-pyyaml==6.0.12.20240311
# via django-stubs # via django-stubs
typing-extensions==4.1.1 typing-extensions==4.12.2
# via # via
# django-stubs # django-stubs
# django-stubs-ext # django-stubs-ext
# mypy # mypy
wheel==0.37.1 wheel==0.43.0
# via pip-tools # via pip-tools
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:

View File

@ -1,8 +1,9 @@
django<4.0 django>=4.0,<5.0
msgpack msgpack
pynacl pynacl
fastapi fastapi>=0.104
pydantic>=2.0.0
typing_extensions typing_extensions
uvicorn[standard] uvicorn[standard]
aiofiles aiofiles
aioredis redis>=4.2.0rc1

View File

@ -1,6 +1,6 @@
coverage coverage
pip-tools pip-tools
pywatchman pywatchman
black ruff
mypy
django-stubs django-stubs
django<4.0

View File

@ -1,67 +1,117 @@
# #
# This file is autogenerated by pip-compile with python 3.10 # This file is autogenerated by pip-compile with Python 3.12
# To update, run: # by the following command:
# #
# pip-compile --output-file=requirements.txt requirements.in/base.txt # pip-compile --output-file=requirements.txt requirements.in/base.txt
# #
aiofiles==0.8.0 aiofiles==23.2.1
# via -r requirements.in/base.txt # via -r requirements.in/base.txt
aioredis==2.0.1 annotated-types==0.7.0
# via -r requirements.in/base.txt # via pydantic
anyio==3.5.0 anyio==4.4.0
# via # via
# httpx
# starlette # starlette
# watchgod # watchfiles
asgiref==3.5.0 asgiref==3.8.1
# via django
certifi==2024.6.2
# via # via
# django # httpcore
# uvicorn # httpx
async-timeout==4.0.2 cffi==1.16.0
# via aioredis
cffi==1.15.0
# via pynacl # via pynacl
click==8.0.4 click==8.1.7
# via uvicorn # via
django==3.2.12 # typer
# uvicorn
django==4.2.14
# via -r requirements.in/base.txt # via -r requirements.in/base.txt
fastapi==0.75.0 dnspython==2.6.1
# via email-validator
email-validator==2.1.1
# via fastapi
fastapi==0.111.0
# via -r requirements.in/base.txt # via -r requirements.in/base.txt
h11==0.13.0 fastapi-cli==0.0.4
# via fastapi
h11==0.14.0
# via
# httpcore
# uvicorn
httpcore==1.0.5
# via httpx
httptools==0.6.1
# via uvicorn # via uvicorn
httptools==0.4.0 httpx==0.27.0
# via uvicorn # via fastapi
idna==3.3 idna==3.7
# via anyio # via
msgpack==1.0.3 # anyio
# email-validator
# httpx
jinja2==3.1.4
# via fastapi
markdown-it-py==3.0.0
# via rich
markupsafe==2.1.5
# via jinja2
mdurl==0.1.2
# via markdown-it-py
msgpack==1.0.8
# via -r requirements.in/base.txt # via -r requirements.in/base.txt
pycparser==2.21 orjson==3.10.3
# via fastapi
pycparser==2.22
# via cffi # via cffi
pydantic==1.9.0 pydantic==2.7.3
# via fastapi
pynacl==1.5.0
# via -r requirements.in/base.txt
python-dotenv==0.19.2
# via uvicorn
pytz==2022.1
# via django
pyyaml==6.0
# via uvicorn
sniffio==1.2.0
# via anyio
sqlparse==0.4.2
# via django
starlette==0.17.1
# via fastapi
typing-extensions==4.1.1
# via # via
# -r requirements.in/base.txt # -r requirements.in/base.txt
# aioredis # fastapi
# pydantic pydantic-core==2.18.4
uvicorn[standard]==0.17.6 # via pydantic
pygments==2.18.0
# via rich
pynacl==1.5.0
# via -r requirements.in/base.txt # via -r requirements.in/base.txt
uvloop==0.16.0 python-dotenv==1.0.1
# via uvicorn # via uvicorn
watchgod==0.8.1 python-multipart==0.0.9
# via fastapi
pyyaml==6.0.1
# via uvicorn # via uvicorn
websockets==10.2 redis==5.1.0b6
# via -r requirements.in/base.txt
rich==13.7.1
# via typer
shellingham==1.5.4
# via typer
sniffio==1.3.1
# via
# anyio
# httpx
sqlparse==0.5.0
# via django
starlette==0.37.2
# via fastapi
typer==0.12.3
# via fastapi-cli
typing-extensions==4.12.2
# via
# -r requirements.in/base.txt
# fastapi
# pydantic
# pydantic-core
# typer
ujson==5.10.0
# via fastapi
uvicorn[standard]==0.30.1
# via
# -r requirements.in/base.txt
# fastapi
uvloop==0.19.0
# via uvicorn
watchfiles==0.22.0
# via uvicorn
websockets==12.0
# via uvicorn # via uvicorn

View File

@ -1,20 +1,20 @@
from setuptools import find_packages, setup from setuptools import find_packages, setup
setup( setup(
name='etebase_server', name="etebase_server",
version='0.9.0', version="0.14.2",
description='An Etebase (EteSync 2.0) server', description="An Etebase (EteSync 2.0) server",
url='https://www.etebase.com/', url="https://www.etebase.com/",
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', "Development Status :: 5 - Production/Stable",
'Framework :: Django', "Framework :: Django",
'Framework :: FastAPI', "Framework :: FastAPI",
'Intended Audience :: System Administrators', "Intended Audience :: System Administrators",
'License :: OSI Approved :: GNU Affero General Public License v3', "License :: OSI Approved :: GNU Affero General Public License v3",
], ],
packages=find_packages(include=['etebase_server', 'etebase_server.*']), packages=find_packages(include=["etebase_server", "etebase_server.*"]),
install_requires=list(open('requirements.in/base.txt')), install_requires=list(open("requirements.in/base.txt")),
package_data={ package_data={
'etebase_server': ['templates/*'], "etebase_server": ["templates/*"],
} },
) )