Author SHA1 Message Date
alve ff392fd066 🏃Add gitea actions demo
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 45s
2025-07-03 20:53:04 +02:00
ponas 4a36cc140c Sort imports 2025-06-05 19:53:56 +02:00
ponas f1ea46188f Make the graphql endpoint CSRF exempt 2025-06-05 19:38:17 +02:00
ponas dd6a559ac6 Add mise.toml 2025-06-05 19:37:59 +02:00
nikolark 5d889ee32c chore(ci): Fix deploy
Ensure uv is on path
2025-05-31 23:13:54 +02:00
nikolark 2b1cdb28ea fix: Fix union syntax 2025-05-30 16:17:45 +02:00
nikolark fbd55bd3a2 fix: strawberry type things 2025-05-30 16:15:18 +02:00
nikolark 71f158da6e chore: Fix most ruff issues
ignore the rest using `uvx ruff check --add-noqa`
2025-05-30 15:43:57 +02:00
nikolark 8b364b0887 chore: Add pre-commit ruff config++ 2025-05-30 15:43:20 +02:00
nikolark 28d747d74d chore(frontend): Roll node to v20 2025-05-30 15:32:55 +02:00
nikolark 6f964a0c57 chore: Roll to python 3.9 to get uv in image 2025-05-30 15:29:41 +02:00
nikolark d256e0eb7e chore: Switch to uv 2025-05-30 15:26:51 +02:00
leander cac189003a increase max saldo to 1000 NOK 2025-01-16 18:25:33 +01:00
alve ff09985944 🥷 Hide users navigation tab 2024-02-15 19:29:13 +00:00
Emma Eilefsen Glenna d76c911d4b fix rendering of register total 2024-01-25 21:45:17 +01:00
Emma Eilefsen Glenna e60473e7ed fix rendering of user balance 2024-01-25 21:44:03 +01:00
Emma Eilefsen Glenna d596c31353 extract function 2024-01-25 21:43:32 +01:00
Emma Eilefsen Glenna 90558a3a28 add comment 2024-01-25 19:01:45 +00:00
alve 57a6c412d5 💄 Hide trailing decimal zeros 2024-01-25 19:01:45 +00:00
alve 19f58211e7 🗃 Make order sum a decimal field 2024-01-25 19:01:45 +00:00
nikolark 0726754678 💡 add some FIXMEs 2024-01-25 19:01:45 +00:00
nikolark 480fba870a 🗃 Replace float fields with int or decimals 2024-01-25 19:01:45 +00:00
alve ed08c60380 💄Fix favicon path 2023-07-19 19:46:01 +02:00
44 changed files with 792 additions and 1333 deletions

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
+5 -8
View File
@@ -2,30 +2,27 @@ variables:
PIP_CACHE_DIR: "${CI_PROJECT_DIR}/.cache/pip" PIP_CACHE_DIR: "${CI_PROJECT_DIR}/.cache/pip"
default: default:
image: nikolaik/python-nodejs:python3.8-nodejs16 image: nikolaik/python-nodejs:python3.9-nodejs20
cache: cache:
paths: paths:
- .cache/pip - .cache/pip
- .venv - .venv
before_script:
- poetry config virtualenvs.in-project true
- poetry install
django-checks: django-checks:
stage: build stage: build
script: script:
- poetry run python manage.py migrate - uv run manage.py migrate
- poetry run python manage.py check - uv run manage.py check
tests: tests:
stage: test stage: test
script: script:
- poetry run bin/test - uv run bin/test
lint: lint:
stage: test stage: test
script: script:
- poetry run bin/lint - uv run bin/pre-commit
deploy: deploy:
stage: deploy stage: deploy
+10 -16
View File
@@ -1,18 +1,12 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: local - repo: https://github.com/astral-sh/ruff-pre-commit
rev: 'v0.11.7'
hooks: hooks:
- id: black - id: ruff
name: black args: [--fix, --exit-non-zero-on-fix]
entry: black - id: ruff-format
args: [--check] - repo: https://github.com/adamchainz/django-upgrade
language: system rev: "1.24.0"
require_serial: true hooks:
types: [python] - id: django-upgrade
- id: isort args: [--target-version, "4.2"]
name: isort
entry: isort
args: [--check]
language: system
types: [python]
+4 -7
View File
@@ -4,11 +4,9 @@ Before starting, make sure you have a working python and nodejs setup with pyenv
```bash ```bash
apt-get install libmysqlclient-dev apt-get install libmysqlclient-dev
poetry install
poetry shell
pre-commit install pre-commit install
python manage.py migrate uv run manage.py migrate
bin/run uv run bin/run
cp .env-default .env cp .env-default .env
# in another terminal # in another terminal
@@ -19,18 +17,17 @@ npm start
## References ## References
- We use JS code generation to work with queries/mutations in our graphql API: https://github.com/dotansimha/graphql-typed-document-node - We use JS code generation to work with queries/mutations in our graphql API: <https://github.com/dotansimha/graphql-typed-document-node>
## TODO ## TODO
- improve product listing (light purple background) - improve product listing (light purple background)
- Stats: products over time - Stats: products over time
- inventory views - inventory views
- Stats: Implement users tab
## TODO-old ## TODO-old
- Larger pictures - Larger pictures
- Compact GUI for max 768 vertical pixels - Compact GUI for max 768 vertical pixels
- Search right of in title - Search right of in title
Executable → Regular
View File
File mode changed.
+9 -2
View File
@@ -1,13 +1,20 @@
from django.contrib import admin from django.contrib import admin
from bbs.models import * from bbs.models import (
InventoryTransaction,
Order,
OrderLine,
Product,
Transaction,
UserProfile,
)
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin): class ProductAdmin(admin.ModelAdmin):
pass pass
admin.site.register(Product, ProductAdmin)
admin.site.register(InventoryTransaction) admin.site.register(InventoryTransaction)
admin.site.register(Order) admin.site.register(Order)
admin.site.register(OrderLine) admin.site.register(OrderLine)
+1 -3
View File
@@ -1,5 +1,3 @@
from typing import List
import strawberry import strawberry
@@ -19,6 +17,6 @@ class FieldError(Error):
@strawberry.type @strawberry.type
class FormErrors: class FormErrors:
fields: List[FieldError] fields: list[FieldError]
message: str = "" message: str = ""
error: bool = True error: bool = True
+13 -8
View File
@@ -1,4 +1,4 @@
from typing import List from typing import Annotated, Union
import strawberry import strawberry
from django.conf import settings from django.conf import settings
@@ -27,16 +27,18 @@ class CreateOrderSuccess:
@strawberry.type @strawberry.type
class InsufficientFunds(Error): class InsufficientFunds(Error): # noqa: N818
amount_lacking: float amount_lacking: float
CreateOrderResponse = strawberry.union("CreateOrderResponse", [CreateOrderSuccess, InsufficientFunds, FormErrors]) CreateOrderResponse = Annotated[
Union[CreateOrderSuccess, InsufficientFunds, FormErrors], strawberry.union("CreateOrderResponse") # noqa: UP007
]
@strawberry.mutation @strawberry.mutation
@transaction.atomic @transaction.atomic
def create_order(customer_id: ID, order_lines: List[OrderLineInput], is_external: bool = False) -> CreateOrderResponse: def create_order(customer_id: ID, order_lines: list[OrderLineInput], is_external: bool = False) -> CreateOrderResponse: # noqa: FBT001
# clean and validate # clean and validate
try: try:
user = UserModel.objects.get(pk=customer_id) user = UserModel.objects.get(pk=customer_id)
@@ -68,7 +70,8 @@ def create_order(customer_id: ID, order_lines: List[OrderLineInput], is_external
if not is_external and user.profile.balance <= order_sum: if not is_external and user.profile.balance <= order_sum:
return InsufficientFunds( return InsufficientFunds(
message="Du har ikke nok penger på bok.", amount_lacking=abs(order_sum - user.profile.balance) message="Du har ikke nok penger på bok.",
amount_lacking=abs(order_sum - user.profile.balance),
) )
# create # create
@@ -91,12 +94,14 @@ class CreateDepositSuccess:
@strawberry.type @strawberry.type
class AboveMaxSaldo(Error): class AboveMaxSaldo(Error): # noqa: N818
max_saldo: float max_saldo: float
above: float above: float
CreateDepositResponse = strawberry.union("CreateDepositResponse", [CreateDepositSuccess, AboveMaxSaldo, FormErrors]) CreateDepositResponse = Annotated[
Union[CreateDepositSuccess, AboveMaxSaldo, FormErrors], strawberry.union("CreateDepositResponse") # noqa: UP007
]
@strawberry.mutation @strawberry.mutation
@@ -118,7 +123,7 @@ def create_deposit(user_id: ID, amount: int) -> CreateDepositResponse:
fields=[FieldError(field="amount", message=message)], fields=[FieldError(field="amount", message=message)],
) )
# 1337 is special <3 # 1337 is special <3
if amount + user.profile.balance > settings.BBS_SALDO_MAX and amount + user.profile.balance != 1337: if amount + user.profile.balance > settings.BBS_SALDO_MAX and amount + user.profile.balance != 1337: # noqa: PLR2004
return AboveMaxSaldo( return AboveMaxSaldo(
message="Du prøver å sette inn for mye penger", message="Du prøver å sette inn for mye penger",
max_saldo=settings.BBS_SALDO_MAX, max_saldo=settings.BBS_SALDO_MAX,
+7 -6
View File
@@ -1,14 +1,12 @@
import strawberry import strawberry
import strawberry_django import strawberry_django
from django.contrib.auth import get_user_model from django.contrib.auth.models import User as DjangoUser
from django.db import transaction from django.db import transaction
from strawberry_django import auto from strawberry import auto
from bbs import models from bbs import models
from bbs.api.types.users import User from bbs.api.types.users import User
DjangoUser = get_user_model()
@strawberry_django.input(DjangoUser) @strawberry_django.input(DjangoUser)
class UserInput: class UserInput:
@@ -19,9 +17,12 @@ class UserInput:
@strawberry.mutation @strawberry.mutation
@transaction.atomic @transaction.atomic
def register_mutation(input: UserInput) -> User: def register_mutation(input: UserInput) -> User: # noqa: A002
user = DjangoUser.objects.create_user( user = DjangoUser.objects.create_user(
first_name=input.first_name, last_name=input.last_name, email=input.email, username=input.email first_name=input.first_name,
last_name=input.last_name,
email=input.email,
username=input.email,
) )
models.UserProfile.objects.create(user=user) models.UserProfile.objects.create(user=user)
return user return user
+1 -3
View File
@@ -1,5 +1,3 @@
from typing import List
import strawberry import strawberry
import strawberry_django import strawberry_django
@@ -8,4 +6,4 @@ from bbs.api.types.inventory import Transaction, TransactionOrdering
@strawberry.type @strawberry.type
class InventoryQueries: class InventoryQueries:
transaction_list: List[Transaction] = strawberry_django.field(pagination=True, order=TransactionOrdering) transaction_list: list[Transaction] = strawberry_django.field(pagination=True, ordering=TransactionOrdering)
+17 -8
View File
@@ -1,17 +1,23 @@
from typing import List
import strawberry import strawberry
import strawberry_django import strawberry_django
from bbs import models from bbs import models
from bbs.api.types.shop import Order, OrderOrdering, OrderStats, OrderStatsByTime, Point, Product, ProductOrderStat from bbs.api.types.shop import (
Order,
OrderOrdering,
OrderStats,
OrderStatsByTime,
Point,
Product,
ProductOrderStat,
)
@strawberry.type @strawberry.type
class ShopQueries: class ShopQueries:
all_products: List[Product] = strawberry_django.field() all_products: list[Product] = strawberry_django.field()
all_orders: List[Order] = strawberry_django.field() all_orders: list[Order] = strawberry_django.field()
order_list: List[Order] = strawberry_django.field(pagination=True, order=OrderOrdering) order_list: list[Order] = strawberry_django.field(pagination=True, ordering=OrderOrdering)
@strawberry.field @strawberry.field
def order_stats(self) -> OrderStats: def order_stats(self) -> OrderStats:
@@ -26,8 +32,11 @@ class ShopQueries:
) )
@strawberry.field @strawberry.field
def product_stats(self) -> List[ProductOrderStat]: def product_stats(self) -> list[ProductOrderStat]:
return [ return [
ProductOrderStat(product_name=stat["product_name"], data=[Point(**p) for p in stat["data"]]) ProductOrderStat(
product_name=stat["product_name"],
data=[Point(**p) for p in stat["data"]],
)
for stat in models.OrderLine.objects.product_order_stats().as_series() for stat in models.OrderLine.objects.product_order_stats().as_series()
] ]
+1 -3
View File
@@ -1,5 +1,3 @@
from typing import List
import strawberry import strawberry
import strawberry_django import strawberry_django
from django.contrib.auth.models import User as DjangoUser from django.contrib.auth.models import User as DjangoUser
@@ -10,7 +8,7 @@ from bbs.api.types.users import User
@strawberry.type @strawberry.type
class UsersQueries: class UsersQueries:
all_users: List[User] = strawberry_django.field() all_users: list[User] = strawberry_django.field()
@strawberry.field @strawberry.field
def user(self, user_id: ID) -> User: def user(self, user_id: ID) -> User:
+2 -2
View File
@@ -1,6 +1,6 @@
import strawberry import strawberry
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from strawberry.extensions import Extension from strawberry.extensions import SchemaExtension
from bbs.api.mutations.shop import ShopMutations from bbs.api.mutations.shop import ShopMutations
from bbs.api.mutations.users import UsersMutations from bbs.api.mutations.users import UsersMutations
@@ -19,7 +19,7 @@ class Mutation(ShopMutations, UsersMutations):
pass pass
class HandleNotFound(Extension): class HandleNotFound(SchemaExtension):
def on_request_end(self): def on_request_end(self):
result = self.execution_context.result result = self.execution_context.result
if result.errors: if result.errors:
+9 -4
View File
@@ -1,11 +1,16 @@
from typing import TYPE_CHECKING, Annotated
import strawberry
import strawberry_django import strawberry_django
from strawberry_django import auto from strawberry import auto
from bbs import models from bbs import models
from bbs.api.types.users import User
if TYPE_CHECKING:
from .users import User
@strawberry_django.ordering.order(models.Transaction) @strawberry_django.order_type(models.Transaction)
class TransactionOrdering: class TransactionOrdering:
created: auto created: auto
@@ -13,6 +18,6 @@ class TransactionOrdering:
@strawberry_django.type(models.Transaction) @strawberry_django.type(models.Transaction)
class Transaction: class Transaction:
id: auto id: auto
user: "User" user: Annotated["User", strawberry.lazy(".users")]
amount: auto amount: auto
created: auto created: auto
+14 -11
View File
@@ -1,14 +1,17 @@
from typing import List from typing import TYPE_CHECKING, Annotated
import strawberry import strawberry
import strawberry_django import strawberry_django
from strawberry_django import auto from strawberry import auto
from bbs import models from bbs import models
from bbs.api.types.utils import JSONScalar from bbs.api.types.utils import JSONScalar
if TYPE_CHECKING:
from .users import User
@strawberry_django.filters.filter(models.Product)
@strawberry_django.filter_type(models.Product)
class ProductFilter: class ProductFilter:
id: auto id: auto
active: auto active: auto
@@ -25,7 +28,7 @@ class Product:
user_counts: JSONScalar user_counts: JSONScalar
@strawberry_django.ordering.order(models.Order) @strawberry_django.order_type(models.Order)
class OrderOrdering: class OrderOrdering:
created: auto created: auto
@@ -33,10 +36,10 @@ class OrderOrdering:
@strawberry_django.type(models.Order) @strawberry_django.type(models.Order)
class Order: class Order:
id: auto id: auto
customer: strawberry.LazyType["User", "bbs.api.types.users"] customer: Annotated["User", strawberry.lazy(".users")]
order_sum: auto order_sum: auto
created: auto created: auto
orderlines: List["OrderLine"] orderlines: list["OrderLine"]
is_external: bool is_external: bool
@@ -57,10 +60,10 @@ class OrderStatsByTime:
@strawberry.type @strawberry.type
class OrderStats: class OrderStats:
yearly: List[OrderStatsByTime] yearly: list[OrderStatsByTime]
monthly: List[OrderStatsByTime] monthly: list[OrderStatsByTime]
daily: List[OrderStatsByTime] daily: list[OrderStatsByTime]
hourly: List[OrderStatsByTime] hourly: list[OrderStatsByTime]
@strawberry.type @strawberry.type
@@ -72,4 +75,4 @@ class Point:
@strawberry.type @strawberry.type
class ProductOrderStat: class ProductOrderStat:
product_name: str product_name: str
data: List[Point] data: list[Point]
+11 -10
View File
@@ -1,16 +1,14 @@
from datetime import datetime from datetime import datetime
from typing import List from decimal import Decimal
import strawberry import strawberry
import strawberry_django import strawberry_django
from django.contrib.auth import get_user_model from django.contrib.auth.models import User as DjangoUser
from strawberry_django import auto from strawberry import auto
from bbs import models from bbs import models
from bbs.api.types.shop import Point, ProductOrderStat from bbs.api.types.shop import Point, ProductOrderStat
UserModel = get_user_model()
@strawberry.type @strawberry.type
class TopMonth: class TopMonth:
@@ -25,21 +23,24 @@ class UserProfile:
balance: auto balance: auto
image: auto image: auto
last_purchase_date: datetime last_purchase_date: datetime
order_sum_total: float order_sum_total: Decimal
@strawberry.field() @strawberry.field()
def top_months(self) -> List[TopMonth]: def top_months(self) -> list[TopMonth]:
return [TopMonth(**month) for month in self.top_months()] return [TopMonth(**month) for month in self.top_months()]
@strawberry.field() @strawberry.field()
def product_order_stats(self) -> List[ProductOrderStat]: def product_order_stats(self) -> list[ProductOrderStat]:
return [ return [
ProductOrderStat(product_name=stat["product_name"], data=[Point(**p) for p in stat["data"]]) ProductOrderStat(
product_name=stat["product_name"],
data=[Point(**p) for p in stat["data"]],
)
for stat in self.product_order_stats().as_series() for stat in self.product_order_stats().as_series()
] ]
@strawberry_django.type(UserModel) @strawberry_django.type(DjangoUser)
class User: class User:
id: auto id: auto
first_name: auto first_name: auto
+10 -2
View File
@@ -2,13 +2,21 @@ from django import forms
from bbs.models import InventoryTransaction, Product from bbs.models import InventoryTransaction, Product
MAX_DIGITS = 9
DECIMAL_PLACES = 1
class InventoryTransactionForm(forms.ModelForm): class InventoryTransactionForm(forms.ModelForm):
unit_price = forms.FloatField(required=True, localize=True) unit_price = forms.DecimalField(
max_digits=MAX_DIGITS,
decimal_places=DECIMAL_PLACES,
required=True,
localize=True,
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Only show active products # Only show active products
super(InventoryTransactionForm, self).__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["product"].queryset = Product.objects.filter(active=True) self.fields["product"].queryset = Product.objects.filter(active=True)
class Meta: class Meta:
+2 -2
View File
@@ -13,11 +13,11 @@ class Command(BaseCommand):
def add_arguments(self, parser: CommandParser): def add_arguments(self, parser: CommandParser):
parser.add_argument("--filename", default="schema.graphql", help="`-` prints to stdout") parser.add_argument("--filename", default="schema.graphql", help="`-` prints to stdout")
def handle(self, *args, filename=None, **options): def handle(self, *args, filename=None, **options): # noqa: ARG002
schema_output = print_schema(schema_symbol) schema_output = print_schema(schema_symbol)
if filename != "-": if filename != "-":
with Path(settings.BASE_DIR, filename).open("w+") as fp: with Path(settings.BASE_DIR, filename).open("w+") as fp:
fp.write(schema_output) fp.write(schema_output)
self.stdout.write(self.style.SUCCESS("OK")) self.stdout.write(self.style.SUCCESS("OK"))
else: else:
print(schema_output) pass
+3 -4
View File
@@ -10,11 +10,10 @@ class Command(BaseCommand):
parser.add_argument("--dry-run", action="store_true") parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--force", action="store_true") parser.add_argument("--force", action="store_true")
def handle(self, *args, dry_run=False, force=False, **options): def handle(self, *args, dry_run=False, force=False, **options): # noqa: ARG002
for profile in UserProfile.objects.all().order_by("id"): for profile in UserProfile.objects.all().order_by("id"):
if dry_run: if dry_run:
print(profile.user.email, profile.image_url()) pass
else: elif force or not profile.image:
if force or not profile.image:
profile.image = profile.image_url() profile.image = profile.image_url()
profile.save() profile.save()
+1 -2
View File
@@ -3,7 +3,6 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
] ]
@@ -180,6 +179,6 @@ class Migration(migrations.Migration):
), ),
migrations.AlterUniqueTogether( migrations.AlterUniqueTogether(
name="orderline", name="orderline",
unique_together=set([("order", "product")]), unique_together={("order", "product")},
), ),
] ]
@@ -0,0 +1,62 @@
# Generated by Django 4.0.2 on 2022-03-05 19:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bbs", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="inventorytransaction",
name="amount",
field=models.IntegerField(verbose_name="Antall"),
),
migrations.AlterField(
model_name="inventorytransaction",
name="unit_price",
field=models.DecimalField(decimal_places=1, max_digits=9, verbose_name="Enhetspris"),
),
migrations.AlterField(
model_name="orderline",
name="unit_price",
field=models.DecimalField(decimal_places=1, max_digits=9, verbose_name="Enhetspris"),
),
migrations.AlterField(
model_name="product",
name="alcohol_percent",
field=models.DecimalField(decimal_places=1, max_digits=9, null=True),
),
migrations.AlterField(
model_name="product",
name="inventory_amount",
field=models.IntegerField(default=0),
),
migrations.AlterField(
model_name="product",
name="sale_price_ext",
field=models.DecimalField(decimal_places=1, max_digits=9),
),
migrations.AlterField(
model_name="product",
name="sale_price_int",
field=models.DecimalField(decimal_places=1, max_digits=9),
),
migrations.AlterField(
model_name="product",
name="volume_liter",
field=models.DecimalField(decimal_places=1, max_digits=9, null=True),
),
migrations.AlterField(
model_name="transaction",
name="amount",
field=models.DecimalField(decimal_places=1, max_digits=9),
),
migrations.AlterField(
model_name="userprofile",
name="balance",
field=models.DecimalField(decimal_places=1, default=0, max_digits=9),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 4.0.2 on 2023-06-28 19:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bbs", "0002_alter_inventorytransaction_amount_and_more"),
]
operations = [
migrations.AlterField(
model_name="order",
name="order_sum",
field=models.DecimalField(db_column="sum", decimal_places=1, max_digits=9),
),
]
+40 -29
View File
@@ -1,7 +1,7 @@
import hashlib import hashlib # noqa: EXE002
from datetime import datetime from datetime import datetime
from decimal import Decimal
from itertools import groupby from itertools import groupby
from typing import Type
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
@@ -17,6 +17,8 @@ from django.db.models.functions import (
) )
EXTERNAL_USER = "Ekstern" EXTERNAL_USER = "Ekstern"
MAX_DIGITS = 9
DECIMAL_PLACES = 1
def _format_counts(per_user): def _format_counts(per_user):
@@ -36,14 +38,15 @@ def _format_counts(per_user):
class Product(models.Model): class Product(models.Model):
name = models.CharField(max_length=64) name = models.CharField(max_length=64)
sale_price_int = models.FloatField() sale_price_int = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
sale_price_ext = models.FloatField() sale_price_ext = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
description = models.CharField(max_length=256, blank=True) description = models.CharField(max_length=256, blank=True)
volume_liter = models.FloatField(null=True) volume_liter = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, null=True)
alcohol_percent = models.FloatField(null=True) alcohol_percent = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, null=True)
image = models.ImageField(upload_to="products", blank=True) image = models.ImageField(upload_to="products", blank=True)
customer_support = models.CharField(max_length=32, blank=True) customer_support = models.CharField(max_length=32, blank=True)
inventory_amount = models.FloatField(default=0) # FIXME: rename to inventory_quantity
inventory_amount = models.IntegerField(default=0)
active = models.BooleanField(default=True) active = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True) updated = models.DateTimeField(auto_now=True)
@@ -83,21 +86,26 @@ class Product(models.Model):
return _format_counts(per_user) return _format_counts(per_user)
def __str__(self): def __str__(self): # noqa: DJ012
return self.name return self.name
class Meta: class Meta: # noqa: DJ012
ordering = ["name"] ordering = ["name"]
class OrderManager(models.Manager): class OrderManager(models.Manager):
def count_by_created(self, extract_class: Type[Extract]): def count_by_created(self, extract_class: type[Extract]):
period = extract_class("created") period = extract_class("created")
return self.get_queryset().annotate(period=period).values("period").annotate(count=Count("id")) return self.get_queryset().annotate(period=period).values("period").annotate(count=Count("id"))
def count_by_created_period(self, period: str): def count_by_created_period(self, period: str):
periods = {"yearly": ExtractYear, "monthly": ExtractMonth, "daily": ExtractIsoWeekDay, "hourly": ExtractHour} periods = {
assert period in periods "yearly": ExtractYear,
"monthly": ExtractMonth,
"daily": ExtractIsoWeekDay,
"hourly": ExtractHour,
}
assert period in periods # noqa: S101
return self.count_by_created(periods[period]) return self.count_by_created(periods[period])
@@ -109,7 +117,7 @@ class Order(models.Model):
blank=True, blank=True,
related_name="orders", related_name="orders",
) )
order_sum = models.FloatField(db_column="sum") order_sum = models.DecimalField(db_column="sum", max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
objects = OrderManager() objects = OrderManager()
@@ -120,7 +128,7 @@ class Order(models.Model):
def reciept(self): def reciept(self):
return ", ".join([str(ol) for ol in self.orderlines.all()]) return ", ".join([str(ol) for ol in self.orderlines.all()])
def __str__(self): def __str__(self): # noqa: DJ012
return f"{self.customer or EXTERNAL_USER}: {self.order_sum} kr" return f"{self.customer or EXTERNAL_USER}: {self.order_sum} kr"
@@ -148,8 +156,10 @@ class OrderLineManager(models.QuerySet):
class OrderLine(models.Model): class OrderLine(models.Model):
order = models.ForeignKey("bbs.Order", on_delete=models.CASCADE, related_name="orderlines") order = models.ForeignKey("bbs.Order", on_delete=models.CASCADE, related_name="orderlines")
product = models.ForeignKey("bbs.Product", on_delete=models.CASCADE, related_name="orderlines") product = models.ForeignKey("bbs.Product", on_delete=models.CASCADE, related_name="orderlines")
# FIXME: rename to quantity
amount = models.IntegerField() amount = models.IntegerField()
unit_price = models.FloatField(verbose_name="Enhetspris") # FIXME: rename to price
unit_price = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, verbose_name="Enhetspris")
objects = OrderLineManager.as_manager() objects = OrderLineManager.as_manager()
@@ -157,10 +167,10 @@ class OrderLine(models.Model):
def price(self): def price(self):
return self.amount * self.unit_price return self.amount * self.unit_price
def __str__(self): def __str__(self): # noqa: DJ012
return f"{self.amount} {self.product} ({self.unit_price} kr per)" return f"{self.amount} {self.product} ({self.unit_price} kr per)"
class Meta: class Meta: # noqa: DJ012
unique_together = ("order", "product") unique_together = ("order", "product")
@@ -168,11 +178,11 @@ class Transaction(models.Model):
"""Money""" """Money"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="transactions") user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="transactions")
amount = models.FloatField() amount = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
def __str__(self): def __str__(self):
return f"{self.id}: { self.user}: {self.amount}" return f"{self.id}: {self.user}: {self.amount}"
class InventoryTransaction(models.Model): class InventoryTransaction(models.Model):
@@ -189,8 +199,10 @@ class InventoryTransaction(models.Model):
related_name="transactions", related_name="transactions",
verbose_name="Produkt", verbose_name="Produkt",
) )
amount = models.FloatField(verbose_name="Antall") # FIXME: rename to quantity
unit_price = models.FloatField(verbose_name="Enhetspris") amount = models.IntegerField(verbose_name="Antall")
# FIXME: rename to price
unit_price = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, verbose_name="Enhetspris")
comment = models.TextField(verbose_name="Kommentar", blank=True) comment = models.TextField(verbose_name="Kommentar", blank=True)
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
@@ -198,13 +210,13 @@ class InventoryTransaction(models.Model):
def price(self): def price(self):
return self.amount * self.unit_price return self.amount * self.unit_price
def __str__(self): def __str__(self): # noqa: DJ012
return f"{self.id}: {self.user}: {self.product}. {self.amount:.0f} stykker ({self.unit_price} kr per)" return f"{self.id}: {self.user}: {self.product}. {self.amount:.0f} stykker ({self.unit_price} kr per)"
class UserProfile(models.Model): class UserProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile") user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile")
balance = models.FloatField(default=0.0) balance = models.DecimalField(max_digits=MAX_DIGITS, decimal_places=DECIMAL_PLACES, default=0)
image = models.URLField(blank=True) image = models.URLField(blank=True)
def last_purchase(self): def last_purchase(self):
@@ -212,8 +224,7 @@ class UserProfile(models.Model):
def last_purchase_date(self) -> datetime: def last_purchase_date(self) -> datetime:
last = self.last_purchase() last = self.last_purchase()
last_p = last.created if last else datetime.min return last.created if last else datetime.min # noqa: DTZ901
return last_p
def top_months(self, limit=5): def top_months(self, limit=5):
return ( return (
@@ -227,21 +238,21 @@ class UserProfile(models.Model):
def product_order_stats(self): def product_order_stats(self):
return OrderLine.objects.filter(order__customer__pk=self.pk).product_order_stats() return OrderLine.objects.filter(order__customer__pk=self.pk).product_order_stats()
def order_sum_total(self) -> float: def order_sum_total(self) -> Decimal:
return Order.objects.filter(customer_id=self.user_id).aggregate(sum=Sum("order_sum"))["sum"] or 0 return Order.objects.filter(customer_id=self.user_id).aggregate(sum=Sum("order_sum"))["sum"] or 0
def image_url(self) -> str: def image_url(self) -> str:
if self.user.email: if self.user.email:
md5_hash = hashlib.md5(self.user.email.lower().strip().encode()).hexdigest() md5_hash = hashlib.md5(self.user.email.lower().strip().encode()).hexdigest() # noqa: S324
return f"https://www.gravatar.com/avatar/{md5_hash}?d=identicon" return f"https://www.gravatar.com/avatar/{md5_hash}?d=identicon"
return "" return ""
def save(self, **kwargs): def save(self, **kwargs): # noqa: DJ012
if not self.image: if not self.image:
self.image = self.image_url() self.image = self.image_url()
super().save(**kwargs) super().save(**kwargs)
def __str__(self): def __str__(self): # noqa: DJ012
return f"{self.user}" return f"{self.user}"
+13 -14
View File
@@ -1,12 +1,13 @@
# Django settings for bbs project. # Django settings for bbs project. # noqa: EXE002
import contextlib
import os import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # noqa: PTH120
DEBUG = True DEBUG = True
ADMINS = ( ADMINS = (
# ('Your Name', 'your_email@example.com'), # ('Your Name', 'your_email@example.com'), # noqa: ERA001
) )
MANAGERS = ADMINS MANAGERS = ADMINS
@@ -20,18 +21,18 @@ DATABASES = {
TIME_ZONE = "Europe/Oslo" TIME_ZONE = "Europe/Oslo"
LANGUAGE_CODE = "nb" LANGUAGE_CODE = "nb"
USE_I18N = True USE_I18N = True
USE_L10N = True USE_TZ = True
SITE_ID = 1 SITE_ID = 1
# Media and uploads # Media and uploads
MEDIA_ROOT = os.path.join(BASE_DIR, "media") MEDIA_ROOT = os.path.join(BASE_DIR, "media") # noqa: PTH118
MEDIA_URL = "/media/" MEDIA_URL = "/media/"
# Static files # Static files
STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles") # noqa: PTH118
STATIC_URL = "/static/" STATIC_URL = "/static/"
STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) STATICFILES_DIRS = (os.path.join(BASE_DIR, "static"),) # noqa: PTH118
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
"django.contrib.staticfiles.finders.FileSystemFinder", "django.contrib.staticfiles.finders.FileSystemFinder",
@@ -39,13 +40,13 @@ STATICFILES_FINDERS = (
) )
# Make this unique, and don't share it with anybody. # Make this unique, and don't share it with anybody.
SECRET_KEY = "osv$uy-ea$*%lc)+xiwt5@2b7au%u*c!pa6hmh#vl)*+tp%fss" SECRET_KEY = "osv$uy-ea$*%lc)+xiwt5@2b7au%u*c!pa6hmh#vl)*+tp%fss" # noqa: S105
TEMPLATES = [ TEMPLATES = [
{ {
"BACKEND": "django.template.backends.django.DjangoTemplates", "BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [ "DIRS": [
os.path.join(BASE_DIR, "templates"), os.path.join(BASE_DIR, "templates"), # noqa: PTH118
], ],
"APP_DIRS": True, "APP_DIRS": True,
"OPTIONS": { "OPTIONS": {
@@ -97,7 +98,7 @@ LOGIN_REDIRECT_URL = "/"
SESSION_COOKIE_AGE = 1209600 # 15 minutes SESSION_COOKIE_AGE = 1209600 # 15 minutes
SESSION_EXPIRE_AT_BROWSER_CLOSE = True SESSION_EXPIRE_AT_BROWSER_CLOSE = True
BBS_SALDO_MAX = 600 BBS_SALDO_MAX = 1000
BBS_LIMIT_DEPOSITS = False BBS_LIMIT_DEPOSITS = False
# Thumbnails # Thumbnails
@@ -141,7 +142,5 @@ CORS_ALLOWED_ORIGINS = [
"http://localhost:3000", "http://localhost:3000",
] ]
try: with contextlib.suppress(ImportError):
from .local_settings import * from .local_settings import * # noqa: F403
except ImportError:
pass
+8 -4
View File
@@ -1,4 +1,4 @@
import pytest import pytest # noqa: INP001
from django.test import Client from django.test import Client
from strawberry.django.test import GraphQLTestClient from strawberry.django.test import GraphQLTestClient
@@ -7,17 +7,21 @@ from bbs.models import Order, Product
@pytest.fixture(name="gql_client") @pytest.fixture(name="gql_client")
def gql_client_fixture(client: Client): def gql_client_fixture(client: Client):
yield GraphQLTestClient(client) return GraphQLTestClient(client)
@pytest.fixture(name="product") @pytest.fixture(name="product")
def product_fix(db): def product_fix(db): # noqa: ARG001
return Product.objects.create(name="borg", sale_price_ext=35, sale_price_int=30) return Product.objects.create(name="borg", sale_price_ext=35, sale_price_int=30)
def test_create_order(product, gql_client): def test_create_order(product, gql_client):
assert Order.objects.count() == 0 assert Order.objects.count() == 0
variables = dict(customerId="external", isExternal=True, orderLines=[{"productId": product.id, "amount": 1}]) variables = {
"customerId": "external",
"isExternal": True,
"orderLines": [{"productId": product.id, "amount": 1}],
}
query = """ query = """
mutation CreateOrder($customerId: ID!, $isExternal: Boolean!, $orderLines: [OrderLineInput!]!) { mutation CreateOrder($customerId: ID!, $isExternal: Boolean!, $orderLines: [OrderLineInput!]!) {
createOrder(customerId: $customerId, isExternal: $isExternal, orderLines: $orderLines) { createOrder(customerId: $customerId, isExternal: $isExternal, orderLines: $orderLines) {
+11 -6
View File
@@ -2,7 +2,8 @@ from django.conf import settings
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.views import LoginView, LogoutView from django.contrib.auth.views import LoginView, LogoutView
from django.urls import path, re_path from django.urls import path
from django.views.decorators.csrf import csrf_exempt
from strawberry.django.views import GraphQLView from strawberry.django.views import GraphQLView
from bbs.api.schema import schema from bbs.api.schema import schema
@@ -12,13 +13,17 @@ admin.autodiscover()
urlpatterns = [ urlpatterns = [
# TODO: migrate reports to SPA # TODO: migrate reports to SPA
re_path(r"^inventory/$", inventory, name="inventory"), path("inventory/", inventory, name="inventory"),
re_path(r"^inventory/add/$", inventory_add, name="inventory-add"), path("inventory/add/", inventory_add, name="inventory-add"),
re_path(r"^report$", report, name="report"), path("report", report, name="report"),
path("accounts/login/", LoginView.as_view(), name="django.contrib.auth.views.login"), path("accounts/login/", LoginView.as_view(), name="django.contrib.auth.views.login"),
path("accounts/logout/", LogoutView.as_view(), name="django.contrib.auth.views.logout"), path(
"accounts/logout/",
LogoutView.as_view(),
name="django.contrib.auth.views.logout",
),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("graphql/", GraphQLView.as_view(schema=schema), name="graphql"), path("graphql/", csrf_exempt(GraphQLView.as_view(schema=schema)), name="graphql"),
] ]
urlpatterns += static("/media", document_root=settings.MEDIA_ROOT) urlpatterns += static("/media", document_root=settings.MEDIA_ROOT)
+13 -14
View File
@@ -10,12 +10,12 @@ from bbs.models import InventoryTransaction, Order, Product, Transaction, UserPr
def report(request): def report(request):
# FIXME: split this up in smaller pieces # FIXME: split this up in smaller pieces
datetime_now = datetime.now() datetime_now = datetime.now() # noqa: DTZ005
start_time = datetime.now() - timedelta(days=30) start_time = datetime.now() - timedelta(days=30) # noqa: DTZ005
# start of day, 1 month # start of day, 1 month
start_time = start_time.replace(hour=0, minute=0, second=0, microsecond=0) start_time = start_time.replace(hour=0, minute=0, second=0, microsecond=0)
# end of day # end of day
end_time = datetime.now().replace(hour=23, minute=59, second=59, microsecond=999) end_time = datetime.now().replace(hour=23, minute=59, second=59, microsecond=999) # noqa: DTZ005
if request.GET.get("start_time", False) or request.GET.get("end_time", False): if request.GET.get("start_time", False) or request.GET.get("end_time", False):
form = DateRangeForm(request.GET) form = DateRangeForm(request.GET)
@@ -50,14 +50,14 @@ def report(request):
k: { k: {
"name": k, "name": k,
"transactions": group, "transactions": group,
"transactions_sum": sum(map(lambda x: x["transactions__price"], group)), "transactions_sum": sum(x["transactions__price"] for x in group),
"transactions_units": sum(map(lambda x: x["transactions__amount"], group)), "transactions_units": sum(x["transactions__amount"] for x in group),
} }
} }
) )
itrans = InventoryTransaction.objects.filter(created__range=[start_time, end_time]) itrans = InventoryTransaction.objects.filter(created__range=[start_time, end_time])
inv_in = sum(map(lambda x: x[1].get("transactions_sum", 0), inventory_products.items())) inv_in = sum(x[1].get("transactions_sum", 0) for x in inventory_products.items())
# out # out
out_products = ( out_products = (
@@ -75,8 +75,8 @@ def report(request):
product = { product = {
"name": k, "name": k,
"orderlines": group, "orderlines": group,
"orderlines_sum": sum(map(lambda x: x["orderlines__price"], group)), "orderlines_sum": sum(x["orderlines__price"] for x in group),
"orderlines_units": sum(map(lambda x: x["orderlines__amount"], group)), "orderlines_units": sum(x["orderlines__amount"] for x in group),
} }
if k in inventory_products: if k in inventory_products:
# add diff # add diff
@@ -88,7 +88,7 @@ def report(request):
p[1]["value_diff"] = p[1].get("transactions_sum", 0) - p[1].get("orderlines_sum", 0) p[1]["value_diff"] = p[1].get("transactions_sum", 0) - p[1].get("orderlines_sum", 0)
p[1]["units_diff"] = p[1].get("transactions_units", 0) - p[1].get("orderlines_units", 0) p[1]["units_diff"] = p[1].get("transactions_units", 0) - p[1].get("orderlines_units", 0)
inv_out = sum(map(lambda x: x[1].get("orderlines_sum", 0), inventory_products.items())) inv_out = sum(x[1].get("orderlines_sum", 0) for x in inventory_products.items())
inv_diff = inv_in - inv_out inv_diff = inv_in - inv_out
@@ -110,7 +110,7 @@ def report(request):
purchases_diff = purchases_end - purchases_start purchases_diff = purchases_end - purchases_start
saldo_diff = saldo_end - saldo_start saldo_diff = saldo_end - saldo_start
return render(request, "report.html", locals()) return render(request, "report.html", locals()) # noqa: DJ003
def inventory(request): def inventory(request):
@@ -120,7 +120,7 @@ def inventory(request):
wholesale_value = sum([p.wholesale_value for p in products_active]) wholesale_value = sum([p.wholesale_value for p in products_active])
num_products = sum(products_active.values_list("inventory_amount", flat=True)) num_products = sum(products_active.values_list("inventory_amount", flat=True))
return render(request, "inventory.html", locals()) return render(request, "inventory.html", locals()) # noqa: DJ003
def inventory_add(request): def inventory_add(request):
@@ -133,13 +133,12 @@ def inventory_add(request):
itrans.user = request.user itrans.user = request.user
itrans.product.save() itrans.product.save()
itrans.save() itrans.save()
messages.success(request, "Satt inn {0} stk {1}.".format(itrans.amount, itrans.product)) messages.success(request, f"Satt inn {itrans.amount} stk {itrans.product}.")
return redirect("inventory-add") return redirect("inventory-add")
else:
messages.error(request, "Feil med skjemaet, se under.") messages.error(request, "Feil med skjemaet, se under.")
form = InventoryTransactionForm(data=request.POST) form = InventoryTransactionForm(data=request.POST)
else: else:
form = InventoryTransactionForm() form = InventoryTransactionForm()
return render(request, "inventory_add.html", locals()) return render(request, "inventory_add.html", locals()) # noqa: DJ003
+5 -3
View File
@@ -1,6 +1,8 @@
#!/bin/bash #!/bin/bash
set -eu set -eu
source "$HOME/.local/bin/env"
export VITE_API_URL="https://bbs.neuf.no/api" export VITE_API_URL="https://bbs.neuf.no/api"
PROJECT_PATH=/var/www/neuf.no/bbs PROJECT_PATH=/var/www/neuf.no/bbs
PROCESS_NAME="bbs.neuf.no" PROCESS_NAME="bbs.neuf.no"
@@ -10,9 +12,9 @@ cd "$PROJECT_PATH" || exit 1
git pull git pull
# Backend # Backend
poetry install uv sync
umask 022; poetry run python manage.py collectstatic --noinput # Collect static umask 022; uv run manage.py collectstatic --noinput # Collect static
poetry run python manage.py migrate # Run DB migrations uv run manage.py migrate # Run DB migrations
# Frontend # Frontend
bin/build-frontend bin/build-frontend
-6
View File
@@ -1,6 +0,0 @@
#!/bin/bash
SRC_DIR="bbs"
black $SRC_DIR
isort $SRC_DIR
-6
View File
@@ -1,6 +0,0 @@
#!/bin/bash
SRC_DIR="bbs"
black --check $SRC_DIR
isort --diff $SRC_DIR
Executable
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
uvx pre-commit run --all-files
+1 -1
View File
@@ -1 +1 @@
v16 v20
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/src/assets/brick.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap" rel="stylesheet">
+1 -2
View File
@@ -52,8 +52,7 @@
"vite-plugin-svgr": "^1.1.0" "vite-plugin-svgr": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">=16", "node": "20"
"npm": ">=7"
} }
}, },
"node_modules/@ampproject/remapping": { "node_modules/@ampproject/remapping": {
+1 -2
View File
@@ -60,7 +60,6 @@
"vite-plugin-svgr": "^1.1.0" "vite-plugin-svgr": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">=16", "node": "20"
"npm": ">=7"
} }
} }
+3 -3
View File
@@ -51,11 +51,11 @@ const Log = () => {
<td> <td>
{(order.orderlines ?? []).map((line) => ( {(order.orderlines ?? []).map((line) => (
<div key={line.id}> <div key={line.id}>
{line.amount}x {line.product.name} {Number(line.amount)}x {line.product.name}
</div> </div>
))} ))}
</td> </td>
<td>{order.orderSum}</td> <td>{Number(order.orderSum)}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -104,7 +104,7 @@ const Log = () => {
<td> <td>
{trans.user.firstName} {trans.user.lastName} {trans.user.firstName} {trans.user.lastName}
</td> </td>
<td>{trans.amount}</td> <td>{Number(trans.amount)}</td>
</tr> </tr>
))} ))}
</tbody> </tbody>
@@ -4,6 +4,7 @@ import { useState } from "react";
import { useToast } from "../../components/ToastProvider"; import { useToast } from "../../components/ToastProvider";
import { AllProductsDocument, AllUsersDocument, CreateOrderDocument, OrderLineInput, Product } from "../../types"; import { AllProductsDocument, AllUsersDocument, CreateOrderDocument, OrderLineInput, Product } from "../../types";
import { activeUserFilter } from "../../utils/users"; import { activeUserFilter } from "../../utils/users";
import {formatCurrency} from "../../utils/currency";
const USER_EXTERNAL = "external"; const USER_EXTERNAL = "external";
@@ -53,7 +54,7 @@ export const useRegister = () => {
setOrder(newOrder); setOrder(newOrder);
}; };
const total = sumTotal(products, order, selectedUser === USER_EXTERNAL); const total = formatCurrency(sumTotal(products, order, selectedUser === USER_EXTERNAL));
const onSubmit = async () => { const onSubmit = async () => {
let res; let res;
+1 -1
View File
@@ -25,7 +25,7 @@ export const StatsNav = () => (
</NavLink> </NavLink>
</li> </li>
<li> <li>
<NavLink className="disabled" to="/stats/users" title="TODO"> <NavLink className="disabled" to="/stats/users" title="TODO" hidden={true}>
Folk Folk
</NavLink> </NavLink>
</li> </li>
+3 -1
View File
@@ -3,6 +3,7 @@ import React, { MouseEventHandler } from "react";
import { AllUsersQuery } from "../../types"; import { AllUsersQuery } from "../../types";
import styles from "./UserCard.module.css"; import styles from "./UserCard.module.css";
import {formatCurrency} from "../../utils/currency";
type UserCardProps = { type UserCardProps = {
user: AllUsersQuery["allUsers"][0] | null; user: AllUsersQuery["allUsers"][0] | null;
@@ -13,6 +14,7 @@ type UserCardProps = {
/** If user is null, then it's the special external user */ /** If user is null, then it's the special external user */
export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.Element => { export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.Element => {
const classes = clsx(styles.user, { [styles.active]: active }); const classes = clsx(styles.user, { [styles.active]: active });
const userBalanceString = formatCurrency(Number(user?.profile?.balance));
return ( return (
<button className={classes} onClick={onUser} type="button" title={user?.firstName || "Ekstern"}> <button className={classes} onClick={onUser} type="button" title={user?.firstName || "Ekstern"}>
<div className={styles.avatarWrap}> <div className={styles.avatarWrap}>
@@ -27,7 +29,7 @@ export const UserCard = ({ user, onUser, active = false }: UserCardProps): JSX.E
</div> </div>
<div className={styles.meta}> <div className={styles.meta}>
{user?.firstName || "Ekstern"} {user?.firstName || "Ekstern"}
<span className={styles.balance}>{user?.profile?.balance || ""}</span> <span className={styles.balance}>{userBalanceString}</span>
</div> </div>
</button> </button>
); );
+7
View File
@@ -0,0 +1,7 @@
export function formatCurrency(value: number) {
if (isNaN(value)) {
return "";
} else {
return value.toFixed(2).replace(/\.00$/, '');
}
}
+3
View File
@@ -0,0 +1,3 @@
[tools]
python = "3.9"
uv = "latest"
Generated
-1103
View File
File diff suppressed because it is too large. Load diff
+47 -35
View File
@@ -1,43 +1,55 @@
[tool.pytest.ini_options] [project]
DJANGO_SETTINGS_MODULE = "bbs.settings"
[tool.black]
line-length = 120
target_version = ['py39']
[tool.isort]
profile = "black"
line_length = 120
[tool.poetry]
name = "bbs" name = "bbs"
version = "0.1.0" version = "0.1.0"
description = "" description = ""
authors = ["Nikolai R Kristiansen <nikolaik@gmail.com>"] authors = [{ name = "Nikolai R Kristiansen", email = "nikolaik@gmail.com" }]
requires-python = ">=3.9,<3.10"
license = "MIT" license = "MIT"
dependencies = [
"django-cors-headers>=3.10.0,<4",
"django-extensions>=3.1.3,<4",
"django-rename-app>=0.1.3,<0.2",
"Django==4.2.*",
"easy-thumbnails>=2.8.1,<3",
"gunicorn>=20.1.0,<21",
"mysqlclient>=2.0.3,<3",
"Pillow>=9.0.0,<10",
"sentry-sdk>=1.5.5,<2",
"strawberry-graphql-django==0.60.*",
"strawberry-graphql==0.270.*",
]
[tool.poetry.dependencies] [tool.pytest.ini_options]
python = "~3.8" DJANGO_SETTINGS_MODULE = "bbs.settings"
django-extensions = "^3.1.3"
Django = "^4.0.0"
easy-thumbnails = "^2.8.1"
gunicorn = "^20.1.0"
mysqlclient = "^2.0.3"
Pillow = "^9.0.0"
sentry-sdk = "^1.5.5"
strawberry-graphql = "^0.97.0"
strawberry-graphql-django = "^0.2.5"
django-cors-headers = "^3.10.0"
django-rename-app = "^0.1.3"
[tool.poetry.dev-dependencies] [dependency-groups]
pytest = "*" dev = [
black = "*" "ruff",
pytest-django = "*" "pytest-django",
pre-commit = "*" "pytest",
isort = "*" ]
[tool.ruff]
line-length = 120
target-version = "py310"
[build-system] [tool.ruff.lint]
requires = ["poetry-core>=1.0.0"] select = ["ALL"]
build-backend = "poetry.core.masonry.api" ignore = [
"D", # pydocstyle: Allow undocumented code
"ANN", # flake8-annotations: Allow untyped code
"TD", "FIX", # Allow TODOs and FIXMEs
"RUF012", # FIXME: Fix later https://docs.astral.sh/ruff/rules/mutable-class-default/
"FBT002", # Allow boolean args
"G004", # Allow logging using f-string
"EM101", "EM102", # Allow string in exceptions (prioritice code readability vs exception time readability)
"TRY003", # Allow long exception messages
"E501", # Allow long lines
"COM812", "ISC001", # Avoid conflicting with ruff formatter
]
[tool.ruff.lint.per-file-ignores]
# Testing code can deviate a bit more
"bbs/tests/*.py" = ["S101", "S106", "PLR2004"]
"**/test_*.py" = ["S101", "S106", "PLR2004"]
"**/tests.py" = ["S101", "S106", "PLR2004"]
Generated
+411
View File
@@ -0,0 +1,411 @@
version = 1
revision = 2
requires-python = "==3.9.*"
[[package]]
name = "asgiref"
version = "3.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
]
[[package]]
name = "bbs"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "django" },
{ name = "django-cors-headers" },
{ name = "django-extensions" },
{ name = "django-rename-app" },
{ name = "easy-thumbnails" },
{ name = "gunicorn" },
{ name = "mysqlclient" },
{ name = "pillow" },
{ name = "sentry-sdk" },
{ name = "strawberry-graphql" },
{ name = "strawberry-graphql-django" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-django" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "django", specifier = "==4.2.*" },
{ name = "django-cors-headers", specifier = ">=3.10.0,<4" },
{ name = "django-extensions", specifier = ">=3.1.3,<4" },
{ name = "django-rename-app", specifier = ">=0.1.3,<0.2" },
{ name = "easy-thumbnails", specifier = ">=2.8.1,<3" },
{ name = "gunicorn", specifier = ">=20.1.0,<21" },
{ name = "mysqlclient", specifier = ">=2.0.3,<3" },
{ name = "pillow", specifier = ">=9.0.0,<10" },
{ name = "sentry-sdk", specifier = ">=1.5.5,<2" },
{ name = "strawberry-graphql", specifier = "==0.270.*" },
{ name = "strawberry-graphql-django", specifier = "==0.60.*" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pytest" },
{ name = "pytest-django" },
{ name = "ruff" },
]
[[package]]
name = "certifi"
version = "2025.4.26"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "django"
version = "4.2.21"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "sqlparse" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c1/bb/2fad5edc1af2945cb499a2e322ac28e4714fc310bd5201ed1f5a9f73a342/django-4.2.21.tar.gz", hash = "sha256:b54ac28d6aa964fc7c2f7335138a54d78980232011e0cd2231d04eed393dcb0d", size = 10424638, upload-time = "2025-05-07T14:07:07.992Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/4f/aeaa3098da18b625ed672f3da6d1cd94e188d1b2cc27c2c841b2f9666282/django-4.2.21-py3-none-any.whl", hash = "sha256:1d658c7bf5d31c7d0cac1cab58bc1f822df89255080fec81909256c30e6180b3", size = 7993839, upload-time = "2025-05-07T14:07:01.318Z" },
]
[[package]]
name = "django-cors-headers"
version = "3.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/bc/f4/8b8c3d5e9a0aeea43576fbe623599052a7699abb54378ddb44adb1ef1ed3/django_cors_headers-3.14.0.tar.gz", hash = "sha256:5fbd58a6fb4119d975754b2bc090f35ec160a8373f276612c675b00e8a138739", size = 24892, upload-time = "2023-02-25T07:20:18.41Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/f1/972b3a183192841b811e4b7496de0a31c09ae1d83a5391743c8ae0cbd86e/django_cors_headers-3.14.0-py3-none-any.whl", hash = "sha256:684180013cc7277bdd8702b80a3c5a4b3fcae4abb2bf134dceb9f5dfe300228e", size = 13187, upload-time = "2023-02-25T07:20:16.376Z" },
]
[[package]]
name = "django-extensions"
version = "3.2.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8a/f1/318684c9466968bf9a9c221663128206e460c1a67f595055be4b284cde8a/django-extensions-3.2.3.tar.gz", hash = "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a", size = 277216, upload-time = "2023-06-05T17:09:01.447Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/7e/ba12b9660642663f5273141018d2bec0a1cae1711f4f6d1093920e157946/django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401", size = 229868, upload-time = "2023-06-05T17:08:58.197Z" },
]
[[package]]
name = "django-rename-app"
version = "0.1.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
]
sdist = { url = "https://files.pythonhosted.org/packages/70/f6/0cb0c7169584e8f87c229e6336ce98f90cbb911ae374e2a7683c25e872b4/django_rename_app-0.1.7.tar.gz", hash = "sha256:f1236dacfbaf06d63aa53a665333c9e0b4943ef41019aae7626dd5fb5ffa69b5", size = 8291, upload-time = "2023-11-08T08:22:05.25Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/4b/6a15f0a25003f7c563218722ee02968f008bedbc4e9d0c08fe9344421d5c/django_rename_app-0.1.7-py2.py3-none-any.whl", hash = "sha256:68ed0dbc9fc167b72cf2f3ba6b464d7981a42a0e09070476cf94f1f603f7a433", size = 8455, upload-time = "2023-11-08T08:22:03.782Z" },
]
[[package]]
name = "easy-thumbnails"
version = "2.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "pillow" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/3b/5fe5c2c20cb08a34759cbd1ca9b8394b121785ebcb033c010a4b27b65a7a/easy_thumbnails-2.10.tar.gz", hash = "sha256:d009462fcd791edf2496e5ed46d044712e1a06b7d4600e4cf2812002e9904ef5", size = 80721, upload-time = "2024-09-12T12:41:00.361Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/ef/de1f3a7421d7a9ce2314ee7bc0d37a8264a50872e2cd5af1240798b0bbba/easy_thumbnails-2.10-py3-none-any.whl", hash = "sha256:9e446fb9da1ca0cbc99bdd1fe8fdd5c38784323d64393a37e16d13cd736980b2", size = 78939, upload-time = "2024-09-12T12:40:59.135Z" },
]
[[package]]
name = "exceptiongroup"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
]
[[package]]
name = "graphql-core"
version = "3.2.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/16/7574029da84834349b60ed71614d66ca3afe46e9bf9c7b9562102acb7d4f/graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab", size = 505353, upload-time = "2025-01-26T16:36:27.374Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/4f/7297663840621022bc73c22d7d9d80dbc78b4db6297f764b545cd5dd462d/graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f", size = 203416, upload-time = "2025-01-26T16:36:24.868Z" },
]
[[package]]
name = "gunicorn"
version = "20.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/28/5b/0d1f0296485a6af03366604142ea8f19f0833894db3512a40ed07b2a56dd/gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8", size = 370601, upload-time = "2021-03-27T01:54:37.202Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/dd/5b190393e6066286773a67dfcc2f9492058e9b57c4867a95f1ba5caf0a83/gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", size = 79531, upload-time = "2021-04-27T12:16:23.375Z" },
]
[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]]
name = "mysqlclient"
version = "2.2.7"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/68/810093cb579daae426794bbd9d88aa830fae296e85172d18cb0f0e5dd4bc/mysqlclient-2.2.7.tar.gz", hash = "sha256:24ae22b59416d5fcce7e99c9d37548350b4565baac82f95e149cac6ce4163845", size = 91383, upload-time = "2025-01-10T12:06:00.763Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0e/e0/524b0777524e0d410f71987f556dd6a0e3273fdb06cd6e91e96afade7220/mysqlclient-2.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:199dab53a224357dd0cb4d78ca0e54018f9cee9bf9ec68d72db50e0a23569076", size = 207857, upload-time = "2025-01-10T11:56:33.666Z" },
{ url = "https://files.pythonhosted.org/packages/20/40/b5d03494c1caa8f4da171db41d8d9d5b0d8959f893761597d97420083362/mysqlclient-2.2.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:977e35244fe6ef44124e9a1c2d1554728a7b76695598e4b92b37dc2130503069", size = 207965, upload-time = "2025-01-10T11:56:37.252Z" },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
]
[[package]]
name = "pillow"
version = "9.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/00/d5/4903f310765e0ff2b8e91ffe55031ac6af77d982f0156061e20a4d1a8b2d/Pillow-9.5.0.tar.gz", hash = "sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1", size = 50488147, upload-time = "2023-04-01T09:31:37.159Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/46/672289c0ff87733fb93854dedf3a8d65642a25c0bfc88e7f6d722f9161a5/Pillow-9.5.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66", size = 3395750, upload-time = "2023-04-01T09:30:10.542Z" },
{ url = "https://files.pythonhosted.org/packages/a9/70/9259e93534d01f846f7d0501f19bb7d8cc1751741bc20826fc8d3a20fe32/Pillow-9.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e", size = 3077133, upload-time = "2023-04-01T09:30:13.386Z" },
{ url = "https://files.pythonhosted.org/packages/95/62/8a943681db5f6588498ed86aa1568dd31c63f6afdabe50841589fc662c68/Pillow-9.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115", size = 3112534, upload-time = "2023-04-01T09:30:15.616Z" },
{ url = "https://files.pythonhosted.org/packages/f2/43/0892913d499c8df2c88dee69d59e77de19e0c51754a9be82023880641c09/Pillow-9.5.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3", size = 3386725, upload-time = "2023-04-01T09:30:19.221Z" },
{ url = "https://files.pythonhosted.org/packages/ff/fc/48a51c0fe2a00d5def57b9981a1e0f8339b516351da7a51500383d833bc8/Pillow-9.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef", size = 3308605, upload-time = "2023-04-01T09:30:29.279Z" },
{ url = "https://files.pythonhosted.org/packages/af/b7/f9faf80e3c93b02712c5748f10c75a8948e74eca61ec2408f7e1d4c9dd16/Pillow-9.5.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705", size = 3164057, upload-time = "2023-04-01T09:30:32.484Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2b/57915b8af178e2c20bfd403ffed4521947881f9dbbfbaba48210dc59b9d7/Pillow-9.5.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1", size = 3415613, upload-time = "2023-04-01T09:30:34.978Z" },
{ url = "https://files.pythonhosted.org/packages/e7/2a/f3ed578595f8486ee2cc07434460097d89aedd406a3db849b890ca8ec416/Pillow-9.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a", size = 3350667, upload-time = "2023-04-01T09:30:37.849Z" },
{ url = "https://files.pythonhosted.org/packages/28/a2/f2d0d584d45100a5419fd70a1233ade8f12469ffe6e8e3acd40364beaadb/Pillow-9.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865", size = 3414552, upload-time = "2023-04-01T09:30:48.682Z" },
{ url = "https://files.pythonhosted.org/packages/51/3a/a6701b987007aaa43559b7d8510629845b25686f09a0eb29f8946a62d767/Pillow-9.5.0-cp39-cp39-win32.whl", hash = "sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964", size = 2229361, upload-time = "2023-04-01T09:30:51.739Z" },
{ url = "https://files.pythonhosted.org/packages/69/72/48cc52bff8731cf72bc4101e34dc44807a410c171f921afb582a511da50e/Pillow-9.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d", size = 2538580, upload-time = "2023-04-01T09:30:54.345Z" },
{ url = "https://files.pythonhosted.org/packages/24/35/92032a00f41bea9bf93f19d48f15daac27d1365c0038fe22dc4e7fc7c8b0/Pillow-9.5.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572", size = 3349772, upload-time = "2023-04-01T09:31:13.438Z" },
{ url = "https://files.pythonhosted.org/packages/50/ce/d39869c22904558ce32e664904cf72f13a9d47703b72392e881d9e7b6082/Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe", size = 3281583, upload-time = "2023-04-01T09:31:16.7Z" },
{ url = "https://files.pythonhosted.org/packages/7a/75/4a382d1567efc6f4e3054f693167f8ce2d1ad939c5f6f12aa5c50f74b997/Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1", size = 3222603, upload-time = "2023-04-01T09:31:19.548Z" },
{ url = "https://files.pythonhosted.org/packages/51/d2/c10f72c44e000d08e41f822083cf322bb59afa7ed01ae7e3e47875b47600/Pillow-9.5.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7", size = 3298174, upload-time = "2023-04-01T09:31:23.005Z" },
{ url = "https://files.pythonhosted.org/packages/02/4a/d362f7f44f1e5801c6726f0eaaeaf869d0d43c554b717072b2c5540cefb4/Pillow-9.5.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799", size = 2538628, upload-time = "2023-04-01T09:31:26.082Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pytest"
version = "8.3.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "tomli" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
]
[[package]]
name = "pytest-django"
version = "4.11.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202, upload-time = "2025-04-03T18:56:09.338Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "six" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
]
[[package]]
name = "ruff"
version = "0.11.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" },
{ url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" },
{ url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" },
{ url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" },
{ url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" },
{ url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" },
{ url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" },
{ url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" },
{ url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" },
{ url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" },
{ url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" },
{ url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" },
{ url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" },
{ url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" },
{ url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" },
{ url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" },
{ url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" },
]
[[package]]
name = "sentry-sdk"
version = "1.45.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c8/28/02c0cd9184f9108e3c52519f9628b215077a3854240e0b17ae845e664855/sentry_sdk-1.45.1.tar.gz", hash = "sha256:a16c997c0f4e3df63c0fc5e4207ccb1ab37900433e0f72fef88315d317829a26", size = 244774, upload-time = "2024-07-26T13:48:32.375Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/9f/105366a122efa93f0cb1914f841747d160788e4d022d0488d2d44c2ba26c/sentry_sdk-1.45.1-py2.py3-none-any.whl", hash = "sha256:608887855ccfe39032bfd03936e3a1c4f4fc99b3a4ac49ced54a4220de61c9c1", size = 267163, upload-time = "2024-07-26T13:48:29.38Z" },
]
[[package]]
name = "setuptools"
version = "80.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
]
[[package]]
name = "six"
version = "1.17.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "sqlparse"
version = "0.5.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
]
[[package]]
name = "strawberry-graphql"
version = "0.270.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "graphql-core" },
{ name = "packaging" },
{ name = "python-dateutil" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/87/07/b4c411266038f9d367ce3307cf28cac74772771dfe6b4a87219311d59c90/strawberry_graphql-0.270.4.tar.gz", hash = "sha256:cca958a6070bcc2aca73a938cd80c0c7b4d39aecc8140a228608025896be990c", size = 207381, upload-time = "2025-05-29T10:40:31.544Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/de/3c7bbd77374fe814d671c607a61d6ba6cc6c25983e7cda6fa391e5c8450b/strawberry_graphql-0.270.4-py3-none-any.whl", hash = "sha256:93c6bd3bde85641ec81655c939b2daa09583e41048f94984a523539c20a8550d", size = 301421, upload-time = "2025-05-29T10:40:28.811Z" },
]
[[package]]
name = "strawberry-graphql-django"
version = "0.60.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref" },
{ name = "django" },
{ name = "strawberry-graphql" },
]
sdist = { url = "https://files.pythonhosted.org/packages/09/83/a0a902db2080469ea5f468d8077063d1c81cca99c89ae0bce6411e2e1bed/strawberry_graphql_django-0.60.0.tar.gz", hash = "sha256:a5d210bd4458be298f86d8b1b8d01ce0208291b5b6b227cd0cfd97c51cf71ee8", size = 84511, upload-time = "2025-05-24T10:01:20.863Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/34/a7/86c098eb3407170230fe95053e1ee0283e5bac5323d90f04e6544fdd6692/strawberry_graphql_django-0.60.0-py3-none-any.whl", hash = "sha256:6c37e9e0fe43a947732f15b43900720171e77f2f66915456a54e18bb32779772", size = 105807, upload-time = "2025-05-24T10:01:19.178Z" },
]
[[package]]
name = "tomli"
version = "2.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
]
[[package]]
name = "typing-extensions"
version = "4.13.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
]
[[package]]
name = "tzdata"
version = "2025.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
]
[[package]]
name = "urllib3"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
]