init
This commit is contained in:
commit
48703ea9fc
36 changed files with 936 additions and 0 deletions
5
.env.example
Normal file
5
.env.example
Normal file
|
@ -0,0 +1,5 @@
|
|||
DATABASE_NAME=postgres
|
||||
DATABASE_USER=postgres
|
||||
DATABASE_PASSWORD=postgres
|
||||
DATABASE_HOST=localhost
|
||||
|
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
local_settings.py
|
||||
__pypackages__/
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
.env
|
||||
venv/
|
1
README.md
Normal file
1
README.md
Normal file
|
@ -0,0 +1 @@
|
|||
# pronouns.cc
|
0
api_v2/__init__.py
Normal file
0
api_v2/__init__.py
Normal file
3
api_v2/admin.py
Normal file
3
api_v2/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
8
api_v2/api.py
Normal file
8
api_v2/api.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from ninja import NinjaAPI
|
||||
|
||||
from .views.users import router as users_router
|
||||
|
||||
api = NinjaAPI()
|
||||
|
||||
api.add_router("/users/", users_router)
|
||||
|
6
api_v2/apps.py
Normal file
6
api_v2/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiV2Config(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "api_v2"
|
0
api_v2/migrations/__init__.py
Normal file
0
api_v2/migrations/__init__.py
Normal file
3
api_v2/models.py
Normal file
3
api_v2/models.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
3
api_v2/tests.py
Normal file
3
api_v2/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
5
api_v2/urls.py
Normal file
5
api_v2/urls.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.urls import path
|
||||
|
||||
from .api import api
|
||||
|
||||
urlpatterns = [path("/", api.urls)]
|
3
api_v2/views.py
Normal file
3
api_v2/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
0
api_v2/views/__init__.py
Normal file
0
api_v2/views/__init__.py
Normal file
38
api_v2/views/users.py
Normal file
38
api_v2/views/users.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
from ninja import Router, Field, Schema
|
||||
from ninja.errors import HttpError
|
||||
|
||||
from pronounscc.models import User
|
||||
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
class UserOut(Schema):
|
||||
id: str = Field(..., alias="uid")
|
||||
username: str
|
||||
display_name: str = Field(..., alias="userprofile.display_name")
|
||||
bio: str = Field(..., alias="userprofile.bio")
|
||||
|
||||
|
||||
@router.get("/@me")
|
||||
def get_me(request):
|
||||
return {"data": "me endpoint, eventually"}
|
||||
|
||||
|
||||
@router.get("/{user_ref}", response=UserOut)
|
||||
def get_user(request, user_ref: str):
|
||||
try:
|
||||
user_id = int(user_ref)
|
||||
try:
|
||||
user = User.objects.get(pk=user_id)
|
||||
return user
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
user = User.objects.get(username=user_ref)
|
||||
return user
|
||||
except User.DoesNotExist:
|
||||
raise HttpError(404, "User not found")
|
0
frontend/__init__.py
Normal file
0
frontend/__init__.py
Normal file
3
frontend/admin.py
Normal file
3
frontend/admin.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
6
frontend/apps.py
Normal file
6
frontend/apps.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FrontendConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "frontend"
|
0
frontend/migrations/__init__.py
Normal file
0
frontend/migrations/__init__.py
Normal file
3
frontend/models.py
Normal file
3
frontend/models.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.db import models
|
||||
|
||||
# Create your models here.
|
3
frontend/tests.py
Normal file
3
frontend/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
3
frontend/views.py
Normal file
3
frontend/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
22
manage.py
Executable file
22
manage.py
Executable file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pronounscc.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
251
poetry.lock
generated
Normal file
251
poetry.lock
generated
Normal file
|
@ -0,0 +1,251 @@
|
|||
# This file is automatically @generated by Poetry and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "asgiref"
|
||||
version = "3.7.2"
|
||||
description = "ASGI specs, helper code, and adapters"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "asgiref-3.7.2-py3-none-any.whl", hash = "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e"},
|
||||
{file = "asgiref-3.7.2.tar.gz", hash = "sha256:9e0ce3aa93a819ba5b45120216b23878cf6e8525eb3848653452b4192b92afed"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "4.2.5"
|
||||
description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "Django-4.2.5-py3-none-any.whl", hash = "sha256:b6b2b5cae821077f137dc4dade696a1c2aa292f892eca28fa8d7bfdf2608ddd4"},
|
||||
{file = "Django-4.2.5.tar.gz", hash = "sha256:5e5c1c9548ffb7796b4a8a4782e9a2e5a3df3615259fc1bfd3ebc73b646146c1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
asgiref = ">=3.6.0,<4"
|
||||
sqlparse = ">=0.3.1"
|
||||
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.extras]
|
||||
argon2 = ["argon2-cffi (>=19.1.0)"]
|
||||
bcrypt = ["bcrypt"]
|
||||
|
||||
[[package]]
|
||||
name = "django-ninja"
|
||||
version = "0.22.2"
|
||||
description = "Django Ninja - Fast Django REST framework"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "django_ninja-0.22.2-py3-none-any.whl", hash = "sha256:c53b098a8190f373ea2605c276a6061d48b2626500a9c6b9916c503e4b4a20eb"},
|
||||
{file = "django_ninja-0.22.2.tar.gz", hash = "sha256:913ebde7571d6a6968c9ac0b9e8a24680c46444d44fdd552f8831dbeede1292c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Django = ">=2.2"
|
||||
pydantic = ">=1.6,<2.0.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit"]
|
||||
doc = ["markdown-include", "mkdocs", "mkdocs-material", "mkdocstrings"]
|
||||
test = ["black", "django-stubs", "flake8", "isort", "mypy (==0.931)", "psycopg2-binary", "pytest", "pytest-asyncio", "pytest-cov", "pytest-django"]
|
||||
|
||||
[[package]]
|
||||
name = "environs"
|
||||
version = "9.5.0"
|
||||
description = "simplified environment variable parsing"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"},
|
||||
{file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
marshmallow = ">=3.0.0"
|
||||
python-dotenv = "*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)", "pytest", "tox"]
|
||||
django = ["dj-database-url", "dj-email-url", "django-cache-url"]
|
||||
lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"]
|
||||
tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "marshmallow"
|
||||
version = "3.20.1"
|
||||
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "marshmallow-3.20.1-py3-none-any.whl", hash = "sha256:684939db93e80ad3561392f47be0230743131560a41c5110684c16e21ade0a5c"},
|
||||
{file = "marshmallow-3.20.1.tar.gz", hash = "sha256:5d2371bbe42000f2b3fb5eaa065224df7d8f8597bc19a1bbfa5bfe7fba8da889"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=17.0"
|
||||
|
||||
[package.extras]
|
||||
dev = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)", "pytest", "pytz", "simplejson", "tox"]
|
||||
docs = ["alabaster (==0.7.13)", "autodocsumm (==0.2.11)", "sphinx (==7.0.1)", "sphinx-issues (==3.0.1)", "sphinx-version-warning (==1.1.2)"]
|
||||
lint = ["flake8 (==6.0.0)", "flake8-bugbear (==23.7.10)", "mypy (==1.4.1)", "pre-commit (>=2.4,<4.0)"]
|
||||
tests = ["pytest", "pytz", "simplejson"]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.1"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
|
||||
{file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg"
|
||||
version = "3.1.10"
|
||||
description = "PostgreSQL database adapter for Python"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "psycopg-3.1.10-py3-none-any.whl", hash = "sha256:8bbeddae5075c7890b2fa3e3553440376d3c5e28418335dee3c3656b06fa2b52"},
|
||||
{file = "psycopg-3.1.10.tar.gz", hash = "sha256:15b25741494344c24066dc2479b0f383dd1b82fa5e75612fa4fa5bb30726e9b6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.1"
|
||||
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
|
||||
[package.extras]
|
||||
binary = ["psycopg-binary (==3.1.10)"]
|
||||
c = ["psycopg-c (==3.1.10)"]
|
||||
dev = ["black (>=23.1.0)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.4.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"]
|
||||
docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"]
|
||||
pool = ["psycopg-pool"]
|
||||
test = ["anyio (>=3.6.2)", "mypy (>=1.4.1)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "1.10.12"
|
||||
description = "Data validation and settings management using python type hints"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pydantic-1.10.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a1fcb59f2f355ec350073af41d927bf83a63b50e640f4dbaa01053a28b7a7718"},
|
||||
{file = "pydantic-1.10.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7ccf02d7eb340b216ec33e53a3a629856afe1c6e0ef91d84a4e6f2fb2ca70fe"},
|
||||
{file = "pydantic-1.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fb2aa3ab3728d950bcc885a2e9eff6c8fc40bc0b7bb434e555c215491bcf48b"},
|
||||
{file = "pydantic-1.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:771735dc43cf8383959dc9b90aa281f0b6092321ca98677c5fb6125a6f56d58d"},
|
||||
{file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ca48477862372ac3770969b9d75f1bf66131d386dba79506c46d75e6b48c1e09"},
|
||||
{file = "pydantic-1.10.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a5e7add47a5b5a40c49b3036d464e3c7802f8ae0d1e66035ea16aa5b7a3923ed"},
|
||||
{file = "pydantic-1.10.12-cp310-cp310-win_amd64.whl", hash = "sha256:e4129b528c6baa99a429f97ce733fff478ec955513630e61b49804b6cf9b224a"},
|
||||
{file = "pydantic-1.10.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b0d191db0f92dfcb1dec210ca244fdae5cbe918c6050b342d619c09d31eea0cc"},
|
||||
{file = "pydantic-1.10.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:795e34e6cc065f8f498c89b894a3c6da294a936ee71e644e4bd44de048af1405"},
|
||||
{file = "pydantic-1.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69328e15cfda2c392da4e713443c7dbffa1505bc9d566e71e55abe14c97ddc62"},
|
||||
{file = "pydantic-1.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2031de0967c279df0d8a1c72b4ffc411ecd06bac607a212892757db7462fc494"},
|
||||
{file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ba5b2e6fe6ca2b7e013398bc7d7b170e21cce322d266ffcd57cca313e54fb246"},
|
||||
{file = "pydantic-1.10.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a7bac939fa326db1ab741c9d7f44c565a1d1e80908b3797f7f81a4f86bc8d33"},
|
||||
{file = "pydantic-1.10.12-cp311-cp311-win_amd64.whl", hash = "sha256:87afda5539d5140cb8ba9e8b8c8865cb5b1463924d38490d73d3ccfd80896b3f"},
|
||||
{file = "pydantic-1.10.12-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:549a8e3d81df0a85226963611950b12d2d334f214436a19537b2efed61b7639a"},
|
||||
{file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da88dfa127b666852bef6d0d796573a8cf5009ffd62104094a4fe39599565"},
|
||||
{file = "pydantic-1.10.12-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba5c4a8552bff16c61882db58544116d021d0b31ee7c66958d14cf386a5b5350"},
|
||||
{file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c79e6a11a07da7374f46970410b41d5e266f7f38f6a17a9c4823db80dadf4303"},
|
||||
{file = "pydantic-1.10.12-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab26038b8375581dc832a63c948f261ae0aa21f1d34c1293469f135fa92972a5"},
|
||||
{file = "pydantic-1.10.12-cp37-cp37m-win_amd64.whl", hash = "sha256:e0a16d274b588767602b7646fa05af2782576a6cf1022f4ba74cbb4db66f6ca8"},
|
||||
{file = "pydantic-1.10.12-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6a9dfa722316f4acf4460afdf5d41d5246a80e249c7ff475c43a3a1e9d75cf62"},
|
||||
{file = "pydantic-1.10.12-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a73f489aebd0c2121ed974054cb2759af8a9f747de120acd2c3394cf84176ccb"},
|
||||
{file = "pydantic-1.10.12-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b30bcb8cbfccfcf02acb8f1a261143fab622831d9c0989707e0e659f77a18e0"},
|
||||
{file = "pydantic-1.10.12-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fcfb5296d7877af406ba1547dfde9943b1256d8928732267e2653c26938cd9c"},
|
||||
{file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2f9a6fab5f82ada41d56b0602606a5506aab165ca54e52bc4545028382ef1c5d"},
|
||||
{file = "pydantic-1.10.12-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dea7adcc33d5d105896401a1f37d56b47d443a2b2605ff8a969a0ed5543f7e33"},
|
||||
{file = "pydantic-1.10.12-cp38-cp38-win_amd64.whl", hash = "sha256:1eb2085c13bce1612da8537b2d90f549c8cbb05c67e8f22854e201bde5d98a47"},
|
||||
{file = "pydantic-1.10.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef6c96b2baa2100ec91a4b428f80d8f28a3c9e53568219b6c298c1125572ebc6"},
|
||||
{file = "pydantic-1.10.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c076be61cd0177a8433c0adcb03475baf4ee91edf5a4e550161ad57fc90f523"},
|
||||
{file = "pydantic-1.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5a58feb9a39f481eda4d5ca220aa8b9d4f21a41274760b9bc66bfd72595b86"},
|
||||
{file = "pydantic-1.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5f805d2d5d0a41633651a73fa4ecdd0b3d7a49de4ec3fadf062fe16501ddbf1"},
|
||||
{file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:1289c180abd4bd4555bb927c42ee42abc3aee02b0fb2d1223fb7c6e5bef87dbe"},
|
||||
{file = "pydantic-1.10.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5d1197e462e0364906cbc19681605cb7c036f2475c899b6f296104ad42b9f5fb"},
|
||||
{file = "pydantic-1.10.12-cp39-cp39-win_amd64.whl", hash = "sha256:fdbdd1d630195689f325c9ef1a12900524dceb503b00a987663ff4f58669b93d"},
|
||||
{file = "pydantic-1.10.12-py3-none-any.whl", hash = "sha256:b749a43aa51e32839c9d71dc67eb1e4221bb04af1033a32e3923d46f9effa942"},
|
||||
{file = "pydantic-1.10.12.tar.gz", hash = "sha256:0fe8a415cea8f340e7a9af9c54fc71a649b43e8ca3cc732986116b3cb135d303"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.2.0"
|
||||
|
||||
[package.extras]
|
||||
dotenv = ["python-dotenv (>=0.10.4)"]
|
||||
email = ["email-validator (>=1.0.3)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.0.0"
|
||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"},
|
||||
{file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "sqlparse"
|
||||
version = "0.4.4"
|
||||
description = "A non-validating SQL parser."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "sqlparse-0.4.4-py3-none-any.whl", hash = "sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3"},
|
||||
{file = "sqlparse-0.4.4.tar.gz", hash = "sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["build", "flake8"]
|
||||
doc = ["sphinx"]
|
||||
test = ["pytest", "pytest-cov"]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.8.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
|
||||
{file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2023.3"
|
||||
description = "Provider of IANA time zone data"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2"
|
||||
files = [
|
||||
{file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"},
|
||||
{file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"},
|
||||
]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "224eac820fafccb151cadbf018af6de7a5cfb05d43fb1b593e0a001c7aa497f4"
|
0
pronounscc/__init__.py
Normal file
0
pronounscc/__init__.py
Normal file
9
pronounscc/admin/__init__.py
Normal file
9
pronounscc/admin/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from django.contrib import admin
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from pronounscc.models import User
|
||||
from .user import UserAdmin
|
||||
|
||||
|
||||
admin.site.register(User, UserAdmin)
|
||||
admin.site.unregister(Group)
|
71
pronounscc/admin/user.py
Normal file
71
pronounscc/admin/user.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from pronounscc.models import User, UserProfile
|
||||
|
||||
|
||||
class UserCreationForm(forms.ModelForm):
|
||||
password1 = forms.CharField(label="Password", widget=forms.PasswordInput)
|
||||
password2 = forms.CharField(
|
||||
label="Password confirmation", widget=forms.PasswordInput
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["username", "email"]
|
||||
|
||||
def clean_password2(self):
|
||||
# Check that the two password entries match
|
||||
password1 = self.cleaned_data.get("password1")
|
||||
password2 = self.cleaned_data.get("password2")
|
||||
if password1 and password2 and password1 != password2:
|
||||
raise ValidationError("Passwords don't match")
|
||||
return password2
|
||||
|
||||
def save(self, commit=True):
|
||||
# Save the provided password in hashed format
|
||||
user = super().save(commit=False)
|
||||
user.set_password(self.cleaned_data["password1"])
|
||||
if commit:
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class UserChangeForm(forms.ModelForm):
|
||||
"""A form for updating users. Includes all the fields on
|
||||
the user, but replaces the password field with admin's
|
||||
disabled password hash display field.
|
||||
"""
|
||||
|
||||
password = ReadOnlyPasswordHashField()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["username", "email", "password", "is_moderator", "is_admin"]
|
||||
|
||||
|
||||
class UserProfileInline(admin.StackedInline):
|
||||
model = UserProfile
|
||||
|
||||
|
||||
class UserAdmin(BaseUserAdmin):
|
||||
form = UserChangeForm
|
||||
add_form = UserCreationForm
|
||||
|
||||
inlines = (UserProfileInline,)
|
||||
|
||||
list_display = ["uid", "username", "is_moderator", "is_admin"]
|
||||
list_filter = ["is_moderator", "is_admin"]
|
||||
fieldsets = [
|
||||
(None, {"fields": ["uid", "username"]}),
|
||||
("Authentication", {"fields": ["email", "discord_username"]}),
|
||||
("Permissions", {"fields": ["is_moderator", "is_admin"]}),
|
||||
]
|
||||
readonly_fields = ["uid", "discord_username"]
|
||||
|
||||
search_fields = ["username", "email"]
|
||||
ordering = ["uid", "username", "email"]
|
||||
filter_horizontal = []
|
16
pronounscc/asgi.py
Normal file
16
pronounscc/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for pronounscc project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pronounscc.settings")
|
||||
|
||||
application = get_asgi_application()
|
84
pronounscc/migrations/0001_initial.py
Normal file
84
pronounscc/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
# Generated by Django 4.2.5 on 2023-09-21 13:59
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="User",
|
||||
fields=[
|
||||
("password", models.CharField(max_length=128, verbose_name="password")),
|
||||
(
|
||||
"last_login",
|
||||
models.DateTimeField(
|
||||
blank=True, null=True, verbose_name="last login"
|
||||
),
|
||||
),
|
||||
(
|
||||
"uid",
|
||||
models.BigIntegerField(
|
||||
primary_key=True, serialize=False, verbose_name="user id"
|
||||
),
|
||||
),
|
||||
("username", models.CharField(unique=True, verbose_name="username")),
|
||||
(
|
||||
"email",
|
||||
models.CharField(
|
||||
blank=True, null=True, unique=True, verbose_name="email"
|
||||
),
|
||||
),
|
||||
(
|
||||
"discord",
|
||||
models.BigIntegerField(
|
||||
null=True, unique=True, verbose_name="discord id"
|
||||
),
|
||||
),
|
||||
(
|
||||
"discord_username",
|
||||
models.TextField(blank=True, verbose_name="discord username"),
|
||||
),
|
||||
(
|
||||
"is_moderator",
|
||||
models.BooleanField(default=False, verbose_name="is moderator"),
|
||||
),
|
||||
(
|
||||
"is_admin",
|
||||
models.BooleanField(default=False, verbose_name="is admin"),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="UserProfile",
|
||||
fields=[
|
||||
(
|
||||
"user",
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
(
|
||||
"display_name",
|
||||
models.TextField(
|
||||
blank=True, max_length=100, verbose_name="display name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"bio",
|
||||
models.TextField(blank=True, max_length=1000, verbose_name="bio"),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
0
pronounscc/migrations/__init__.py
Normal file
0
pronounscc/migrations/__init__.py
Normal file
1
pronounscc/models/__init__.py
Normal file
1
pronounscc/models/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
from .user import User, UserProfile
|
79
pronounscc/models/user.py
Normal file
79
pronounscc/models/user.py
Normal file
|
@ -0,0 +1,79 @@
|
|||
from django.contrib.auth.models import BaseUserManager, AbstractBaseUser
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pronounscc.snowflake import Snowflake
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
def create_user(
|
||||
self,
|
||||
username: str,
|
||||
email: str | None,
|
||||
discord: tuple[int, str] | None,
|
||||
password=None,
|
||||
):
|
||||
if (not email and not password) and not discord:
|
||||
raise ValueError("Users must have an authentication method")
|
||||
|
||||
user = self.model(
|
||||
username=username,
|
||||
email=self.normalize_email(email) if email else None,
|
||||
discord=discord[0] if discord else None,
|
||||
discord_username=discord[1] if discord else None,
|
||||
)
|
||||
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_superuser(self, username: str, email: str, password=None):
|
||||
user = self.model(
|
||||
username=username,
|
||||
email=self.normalize_email(email),
|
||||
)
|
||||
|
||||
user.set_password(password)
|
||||
user.is_admin = True
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
|
||||
class User(AbstractBaseUser):
|
||||
"""The basic user model that handles authentication.
|
||||
|
||||
User profile data is in UserProfile instead."""
|
||||
|
||||
uid = models.BigIntegerField(_("user id"), primary_key=True, default=Snowflake.generate)
|
||||
username = models.CharField(_("username"), unique=True)
|
||||
email = models.CharField(_("email"), unique=True, blank=True, null=True)
|
||||
|
||||
discord = models.BigIntegerField(_("discord id"), unique=True, null=True)
|
||||
discord_username = models.CharField(_("discord username"), blank=True)
|
||||
|
||||
is_moderator = models.BooleanField(_("is moderator"), default=False)
|
||||
is_admin = models.BooleanField(_("is admin"), default=False)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
USERNAME_FIELD = "username"
|
||||
EMAIL_FIELD = "email"
|
||||
REQUIRED_FIELDS = ["email"]
|
||||
|
||||
@property
|
||||
def is_staff(self):
|
||||
return self.is_moderator or self.is_admin
|
||||
|
||||
def has_perm(self, perm, obj=None):
|
||||
return self.is_staff
|
||||
|
||||
def has_module_perms(self, app_label):
|
||||
return self.is_staff
|
||||
|
||||
|
||||
|
||||
class UserProfile(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||
|
||||
display_name = models.CharField(_("display name"), max_length=100, blank=True)
|
||||
bio = models.TextField(_("bio"), max_length=1000, blank=True)
|
138
pronounscc/settings.py
Normal file
138
pronounscc/settings.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
"""
|
||||
Django settings for pronounscc project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.2.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from environs import Env
|
||||
|
||||
env = Env()
|
||||
env.read_env()
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = env("SECRET_KEY")
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = env.bool("DEBUG", False)
|
||||
|
||||
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", [])
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"pronounscc",
|
||||
"api_v2",
|
||||
"frontend",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "pronounscc.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = "pronounscc.wsgi.application"
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"NAME": env("DATABASE_NAME", "postgres"),
|
||||
"USER": env("DATABASE_USER", "postgres"),
|
||||
"PASSWORD": env("DATABASE_PASSWORD", "postgres"),
|
||||
"HOST": env("DATABASE_HOST", "localhost"),
|
||||
"PORT": env("DATABASE_PORT", "5432"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
# Authentication
|
||||
|
||||
AUTH_USER_MODEL = "pronounscc.User"
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = "static/"
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
105
pronounscc/snowflake.py
Normal file
105
pronounscc/snowflake.py
Normal file
|
@ -0,0 +1,105 @@
|
|||
import os
|
||||
import threading
|
||||
from datetime import datetime, timezone
|
||||
from random import randrange
|
||||
|
||||
_local = threading.local()
|
||||
|
||||
def _get_increment() -> int:
|
||||
if not hasattr(_local, "increment"):
|
||||
_local.increment = randrange(0, 4095)
|
||||
|
||||
increment = _local.increment
|
||||
_local.increment += 1
|
||||
return increment
|
||||
|
||||
|
||||
class Snowflake:
|
||||
"""A Snowflake ID (https://en.wikipedia.org/wiki/Snowflake_ID).
|
||||
This class wraps an integer and adds convenience functions."""
|
||||
|
||||
EPOCH = 1_640_995_200_000 # 2022-01-01 at 00:00:00 UTC
|
||||
|
||||
_raw: int
|
||||
|
||||
def __init__(self, src: int):
|
||||
self._raw = src
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.id)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Snowflake<{self.id}, {self.process}, {self.thread}, {self.increment}, {self.timestamp}>"
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self._raw
|
||||
|
||||
def __float__(self) -> float:
|
||||
return float(self._raw)
|
||||
|
||||
def __lt__(self, y: "Snowflake"):
|
||||
return self.id < y.id
|
||||
|
||||
def __le__(self, y: "Snowflake"):
|
||||
return self.id <= y.id
|
||||
|
||||
def __eq__(self, y: "Snowflake"):
|
||||
return self.id == y.id
|
||||
|
||||
def __ne__(self, y: "Snowflake"):
|
||||
return self.id != y.id
|
||||
|
||||
def __gt__(self, y: "Snowflake"):
|
||||
return self.id > y.id
|
||||
|
||||
def __ge__(self, y: "Snowflake"):
|
||||
return self.id >= y.id
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
"""The raw integer value of the snowflake."""
|
||||
return self._raw
|
||||
|
||||
@property
|
||||
def time(self) -> datetime:
|
||||
"""The time embedded into the snowflake."""
|
||||
return datetime.fromtimestamp(self.timestamp, tz=timezone.utc)
|
||||
|
||||
@property
|
||||
def timestamp(self) -> float:
|
||||
"""The unix timestamp embedded into the snowflake."""
|
||||
return ((self._raw >> 22) + self.EPOCH) / 1000
|
||||
|
||||
@property
|
||||
def process(self) -> int:
|
||||
"""The process ID embedded into the snowflake."""
|
||||
return (self._raw & 0x3E0000) >> 17
|
||||
|
||||
@property
|
||||
def thread(self) -> int:
|
||||
"""The thread ID embedded into the snowflake."""
|
||||
return (self._raw & 0x1F000) >> 12
|
||||
|
||||
@property
|
||||
def increment(self) -> int:
|
||||
"""The increment embedded into the snowflake."""
|
||||
return self._raw & 0xFFF
|
||||
|
||||
@classmethod
|
||||
def generate(cls, time: datetime | None = None):
|
||||
"""Generates a new snowflake.
|
||||
If `time` is set, use that time for the snowflake, otherwise, use the current time.
|
||||
"""
|
||||
|
||||
process_id = os.getpid()
|
||||
thread_id = threading.get_native_id()
|
||||
increment = _get_increment()
|
||||
now = time if time else datetime.now(tz=timezone.utc)
|
||||
timestamp = round(now.timestamp() * 1000) - cls.EPOCH
|
||||
|
||||
return cls(
|
||||
timestamp << 22
|
||||
| (process_id % 32) << 17
|
||||
| (thread_id % 32) << 12
|
||||
| (increment % 4096)
|
||||
)
|
23
pronounscc/urls.py
Normal file
23
pronounscc/urls.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
"""
|
||||
URL configuration for pronounscc project.
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/4.2/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.urls import include, path
|
||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path("api/v2", include("api_v2.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
]
|
16
pronounscc/wsgi.py
Normal file
16
pronounscc/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for pronounscc project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pronounscc.settings")
|
||||
|
||||
application = get_wsgi_application()
|
19
pyproject.toml
Normal file
19
pyproject.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[tool.poetry]
|
||||
name = "pronounscc"
|
||||
version = "0.1.0"
|
||||
description = "Pronoun website and API"
|
||||
authors = ["sam <sam@sleepycat.moe>"]
|
||||
license = "AGPL-3.0-only"
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
django = "^4.2.5"
|
||||
environs = "^9.5.0"
|
||||
psycopg = "^3.1.10"
|
||||
django-ninja = "^0.22.2"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
Loading…
Reference in a new issue