mirror of
https://github.com/ae-utbm/sith.git
synced 2026-05-01 02:56:02 +00:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c17337595 | |||
| da621f6d7e | |||
| 6b2b4d347f | |||
| 26483825a1 | |||
| 9ef592849c | |||
| c329fb1d0e | |||
| ef611ce1a7 | |||
| fb23d24db7 | |||
| 16ca6c3ead | |||
| f8f85c2778 | |||
| cf40e941d4 | |||
| adeadeaf70 | |||
| 40fce1609d | |||
| 0f02d55318 | |||
| 965309d76c | |||
| 26c6ab4a9f | |||
| 2613ede59d | |||
| 6634bd987d | |||
| 1c6d7f435a | |||
| f9c5297473 | |||
| 52117b5a24 | |||
| ae72a2e00f | |||
| fdf89ea716 | |||
| 3954f2f170 | |||
| d36d672d0b | |||
| da3602329c | |||
|
8b18999514
|
|||
| 1d525ca6d4 | |||
|
4dea60ac66
|
@@ -12,7 +12,7 @@ runs:
|
||||
steps:
|
||||
- name: Install apt packages
|
||||
if: ${{ inputs.full == 'true' }}
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
|
||||
with:
|
||||
packages: gettext
|
||||
version: 1.0 # increment to reset cache
|
||||
@@ -23,26 +23,29 @@ runs:
|
||||
with:
|
||||
redis-version: "7.x"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
version: "0.5.14"
|
||||
enable-cache: true
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: "Set up Python"
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Restore cached virtualenv
|
||||
uses: actions/cache/restore@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v8.1.0
|
||||
with:
|
||||
version: "0.11.8"
|
||||
enable-cache: false
|
||||
cache-dependency-glob: "uv.lock"
|
||||
|
||||
- name: Restore cached virtualenv
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('.python-version') }}-${{ hashFiles('pyproject.toml') }}-${{ env.CACHE_SUFFIX }}
|
||||
path: .venv
|
||||
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
|
||||
restore-keys: |
|
||||
uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
|
||||
uv-${{ runner.os }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
run: uv sync --locked
|
||||
shell: bash
|
||||
|
||||
- name: Install Xapian
|
||||
@@ -50,15 +53,6 @@ runs:
|
||||
run: uv run ./manage.py install_xapian
|
||||
shell: bash
|
||||
|
||||
# compiling xapian accounts for almost the entirety of the virtualenv setup,
|
||||
# so we save the virtual environment only on workflows where it has been installed
|
||||
- name: Save cached virtualenv
|
||||
if: ${{ inputs.full == 'true' }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
key: venv-${{ runner.os }}-${{ hashFiles('.python-version') }}-${{ hashFiles('pyproject.toml') }}-${{ env.CACHE_SUFFIX }}
|
||||
path: .venv
|
||||
|
||||
- name: Compile gettext messages
|
||||
if: ${{ inputs.full == 'true' }}
|
||||
run: uv run ./manage.py compilemessages
|
||||
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
name: Launch pre-commits checks (ruff)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
- uses: pre-commit/action@v3.0.1
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
pytest-mark: [not slow]
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- uses: ./.github/actions/setup_project
|
||||
with:
|
||||
full: true
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
uv run coverage report
|
||||
uv run coverage html
|
||||
- name: Archive code coverage results
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage-report-${{ matrix.pytest-mark }}
|
||||
path: coverage_report
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: SSH Remote Commands
|
||||
uses: appleboy/ssh-action@v1.1.0
|
||||
uses: appleboy/ssh-action@v1.2.5
|
||||
with:
|
||||
# Proxy
|
||||
proxy_host : ${{secrets.PROXY_HOST}}
|
||||
@@ -29,8 +29,6 @@ jobs:
|
||||
username : ${{secrets.USER}}
|
||||
key: ${{secrets.KEY}}
|
||||
|
||||
script_stop: true
|
||||
|
||||
# See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
|
||||
script: |
|
||||
cd ${{secrets.SITH_PATH}}
|
||||
|
||||
@@ -9,10 +9,10 @@ jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
- uses: ./.github/actions/setup_project
|
||||
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v5
|
||||
with:
|
||||
key: mkdocs-material-${{ env.cache_id }}
|
||||
path: .cache
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: SSH Remote Commands
|
||||
uses: appleboy/ssh-action@v1.1.0
|
||||
uses: appleboy/ssh-action@v1.2.5
|
||||
with:
|
||||
# Proxy
|
||||
proxy_host : ${{secrets.PROXY_HOST}}
|
||||
@@ -28,8 +28,6 @@ jobs:
|
||||
username : ${{secrets.USER}}
|
||||
key: ${{secrets.KEY}}
|
||||
|
||||
script_stop: true
|
||||
|
||||
# See https://github.com/ae-utbm/sith/wiki/GitHub-Actions#deployment-action
|
||||
script: |
|
||||
cd ${{secrets.SITH_PATH}}
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ class GroupController(ControllerBase):
|
||||
)
|
||||
@paginate(PageNumberPaginationExtra, page_size=50)
|
||||
def search_group(self, search: Annotated[str, MinLen(1)]):
|
||||
return Group.objects.filter(name__icontains=search).values()
|
||||
return Group.objects.filter(name__icontains=search).order_by("name").values()
|
||||
|
||||
|
||||
DepthValue = Annotated[int, Ge(0), Le(10)]
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
|
||||
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
|
||||
# Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
#
|
||||
#
|
||||
@@ -41,7 +41,14 @@ from com.ics_calendar import IcsCalendar
|
||||
from com.models import News, NewsDate, Sith, Weekmail
|
||||
from core.models import BanGroup, Group, Page, PageRev, SithFile, User
|
||||
from core.utils import resize_image
|
||||
from counter.models import Counter, Product, ProductType, ReturnableProduct, StudentCard
|
||||
from counter.models import (
|
||||
Counter,
|
||||
Price,
|
||||
Product,
|
||||
ProductType,
|
||||
ReturnableProduct,
|
||||
StudentCard,
|
||||
)
|
||||
from election.models import Candidature, Election, ElectionList, Role
|
||||
from forum.models import Forum
|
||||
from pedagogy.models import UE
|
||||
@@ -110,7 +117,9 @@ class Command(BaseCommand):
|
||||
p.save(force_lock=True)
|
||||
|
||||
club_root = SithFile.objects.create(name="clubs", owner=root)
|
||||
sas = SithFile.objects.create(name="SAS", owner=root)
|
||||
sas = SithFile.objects.create(
|
||||
name="SAS", owner=root, id=settings.SITH_SAS_ROOT_DIR_ID
|
||||
)
|
||||
main_club = Club.objects.create(
|
||||
id=1, name="AE", address="6 Boulevard Anatole France, 90000 Belfort"
|
||||
)
|
||||
@@ -372,125 +381,15 @@ class Command(BaseCommand):
|
||||
end_date=localdate() - timedelta(days=100),
|
||||
)
|
||||
|
||||
p = ProductType.objects.create(name="Bières bouteilles")
|
||||
c = ProductType.objects.create(name="Cotisations")
|
||||
r = ProductType.objects.create(name="Rechargements")
|
||||
verre = ProductType.objects.create(name="Verre")
|
||||
cotis = Product.objects.create(
|
||||
name="Cotis 1 semestre",
|
||||
code="1SCOTIZ",
|
||||
product_type=c,
|
||||
purchase_price="15",
|
||||
selling_price="15",
|
||||
special_selling_price="15",
|
||||
club=main_club,
|
||||
)
|
||||
cotis2 = Product.objects.create(
|
||||
name="Cotis 2 semestres",
|
||||
code="2SCOTIZ",
|
||||
product_type=c,
|
||||
purchase_price="28",
|
||||
selling_price="28",
|
||||
special_selling_price="28",
|
||||
club=main_club,
|
||||
)
|
||||
refill = Product.objects.create(
|
||||
name="Rechargement 15 €",
|
||||
code="15REFILL",
|
||||
product_type=r,
|
||||
purchase_price="15",
|
||||
selling_price="15",
|
||||
special_selling_price="15",
|
||||
club=main_club,
|
||||
)
|
||||
barb = Product.objects.create(
|
||||
name="Barbar",
|
||||
code="BARB",
|
||||
product_type=p,
|
||||
purchase_price="1.50",
|
||||
selling_price="1.7",
|
||||
special_selling_price="1.6",
|
||||
club=main_club,
|
||||
limit_age=18,
|
||||
)
|
||||
cble = Product.objects.create(
|
||||
name="Chimay Bleue",
|
||||
code="CBLE",
|
||||
product_type=p,
|
||||
purchase_price="1.50",
|
||||
selling_price="1.7",
|
||||
special_selling_price="1.6",
|
||||
club=main_club,
|
||||
limit_age=18,
|
||||
)
|
||||
cons = Product.objects.create(
|
||||
name="Consigne Eco-cup",
|
||||
code="CONS",
|
||||
product_type=verre,
|
||||
purchase_price="1",
|
||||
selling_price="1",
|
||||
special_selling_price="1",
|
||||
club=main_club,
|
||||
)
|
||||
dcons = Product.objects.create(
|
||||
name="Déconsigne Eco-cup",
|
||||
code="DECO",
|
||||
product_type=verre,
|
||||
purchase_price="-1",
|
||||
selling_price="-1",
|
||||
special_selling_price="-1",
|
||||
club=main_club,
|
||||
)
|
||||
cors = Product.objects.create(
|
||||
name="Corsendonk",
|
||||
code="CORS",
|
||||
product_type=p,
|
||||
purchase_price="1.50",
|
||||
selling_price="1.7",
|
||||
special_selling_price="1.6",
|
||||
club=main_club,
|
||||
limit_age=18,
|
||||
)
|
||||
carolus = Product.objects.create(
|
||||
name="Carolus",
|
||||
code="CARO",
|
||||
product_type=p,
|
||||
purchase_price="1.50",
|
||||
selling_price="1.7",
|
||||
special_selling_price="1.6",
|
||||
club=main_club,
|
||||
limit_age=18,
|
||||
)
|
||||
Product.objects.create(
|
||||
name="remboursement",
|
||||
code="REMBOURS",
|
||||
purchase_price="0",
|
||||
selling_price="0",
|
||||
special_selling_price="0",
|
||||
club=refound,
|
||||
)
|
||||
groups.subscribers.products.add(
|
||||
cotis, cotis2, refill, barb, cble, cors, carolus
|
||||
)
|
||||
groups.old_subscribers.products.add(cotis, cotis2)
|
||||
|
||||
mde = Counter.objects.get(name="MDE")
|
||||
mde.products.add(barb, cble, cons, dcons)
|
||||
|
||||
eboutic = Counter.objects.get(name="Eboutic")
|
||||
eboutic.products.add(barb, cotis, cotis2, refill)
|
||||
self._create_products(groups, main_club, refound)
|
||||
|
||||
Counter.objects.create(name="Carte AE", club=refound, type="OFFICE")
|
||||
|
||||
ReturnableProduct.objects.create(
|
||||
product=cons, returned_product=dcons, max_return=3
|
||||
)
|
||||
|
||||
# Add barman to counter
|
||||
Counter.sellers.through.objects.bulk_create(
|
||||
[
|
||||
Counter.sellers.through(counter_id=2, user=krophil),
|
||||
Counter.sellers.through(counter=mde, user=skia),
|
||||
Counter.sellers.through(counter_id=1, user=skia), # MDE
|
||||
Counter.sellers.through(counter_id=2, user=krophil), # Foyer
|
||||
]
|
||||
)
|
||||
|
||||
@@ -746,6 +645,131 @@ class Command(BaseCommand):
|
||||
]
|
||||
)
|
||||
|
||||
def _create_products(
|
||||
self, groups: PopulatedGroups, main_club: Club, refound_club: Club
|
||||
):
|
||||
beers_type, cotis_type, refill_type, verre_type = (
|
||||
ProductType.objects.bulk_create(
|
||||
[
|
||||
ProductType(name="Bières bouteilles"),
|
||||
ProductType(name="Cotisations"),
|
||||
ProductType(name="Rechargements"),
|
||||
ProductType(name="Verre"),
|
||||
]
|
||||
)
|
||||
)
|
||||
cotis = Product.objects.create(
|
||||
name="Cotis 1 semestre",
|
||||
code="1SCOTIZ",
|
||||
product_type=cotis_type,
|
||||
purchase_price=15,
|
||||
club=main_club,
|
||||
)
|
||||
cotis2 = Product.objects.create(
|
||||
name="Cotis 2 semestres",
|
||||
code="2SCOTIZ",
|
||||
product_type=cotis_type,
|
||||
purchase_price="28",
|
||||
club=main_club,
|
||||
)
|
||||
refill = Product.objects.create(
|
||||
name="Rechargement 15 €",
|
||||
code="15REFILL",
|
||||
product_type=refill_type,
|
||||
purchase_price=15,
|
||||
club=main_club,
|
||||
)
|
||||
barb = Product.objects.create(
|
||||
name="Barbar",
|
||||
code="BARB",
|
||||
product_type=beers_type,
|
||||
purchase_price="1.50",
|
||||
club=main_club,
|
||||
limit_age=18,
|
||||
)
|
||||
cble = Product.objects.create(
|
||||
name="Chimay Bleue",
|
||||
code="CBLE",
|
||||
product_type=beers_type,
|
||||
purchase_price="1.50",
|
||||
club=main_club,
|
||||
limit_age=18,
|
||||
)
|
||||
cons = Product.objects.create(
|
||||
name="Consigne Eco-cup",
|
||||
code="CONS",
|
||||
product_type=verre_type,
|
||||
purchase_price="1",
|
||||
club=main_club,
|
||||
)
|
||||
dcons = Product.objects.create(
|
||||
name="Déconsigne Eco-cup",
|
||||
code="DECO",
|
||||
product_type=verre_type,
|
||||
purchase_price="-1",
|
||||
club=main_club,
|
||||
)
|
||||
cors = Product.objects.create(
|
||||
name="Corsendonk",
|
||||
code="CORS",
|
||||
product_type=beers_type,
|
||||
purchase_price="1.50",
|
||||
club=main_club,
|
||||
limit_age=18,
|
||||
)
|
||||
carolus = Product.objects.create(
|
||||
name="Carolus",
|
||||
code="CARO",
|
||||
product_type=beers_type,
|
||||
purchase_price="1.50",
|
||||
club=main_club,
|
||||
limit_age=18,
|
||||
)
|
||||
Product.objects.create(
|
||||
name="remboursement",
|
||||
code="REMBOURS",
|
||||
purchase_price=0,
|
||||
club=refound_club,
|
||||
)
|
||||
ReturnableProduct.objects.create(
|
||||
product=cons, returned_product=dcons, max_return=3
|
||||
)
|
||||
mde = Counter.objects.get(name="MDE")
|
||||
mde.products.add(barb, cble, cons, dcons)
|
||||
eboutic = Counter.objects.get(name="Eboutic")
|
||||
eboutic.products.add(barb, cotis, cotis2, refill)
|
||||
|
||||
cotis, cotis2, refill, barb, cble, cors, carolus, cons, dcons = (
|
||||
Price.objects.bulk_create(
|
||||
[
|
||||
Price(product=cotis, amount=15),
|
||||
Price(product=cotis2, amount=28),
|
||||
Price(product=refill, amount=15),
|
||||
Price(product=barb, amount=1.7),
|
||||
Price(product=cble, amount=1.7),
|
||||
Price(product=cors, amount=1.7),
|
||||
Price(product=carolus, amount=1.7),
|
||||
Price(product=cons, amount=1),
|
||||
Price(product=dcons, amount=-1),
|
||||
]
|
||||
)
|
||||
)
|
||||
Price.groups.through.objects.bulk_create(
|
||||
[
|
||||
Price.groups.through(price=cotis, group=groups.subscribers),
|
||||
Price.groups.through(price=cotis2, group=groups.subscribers),
|
||||
Price.groups.through(price=refill, group=groups.subscribers),
|
||||
Price.groups.through(price=barb, group=groups.subscribers),
|
||||
Price.groups.through(price=cble, group=groups.subscribers),
|
||||
Price.groups.through(price=cors, group=groups.subscribers),
|
||||
Price.groups.through(price=carolus, group=groups.subscribers),
|
||||
Price.groups.through(price=cotis, group=groups.old_subscribers),
|
||||
Price.groups.through(price=cotis2, group=groups.old_subscribers),
|
||||
Price.groups.through(price=cons, group=groups.old_subscribers),
|
||||
Price.groups.through(price=dcons, group=groups.old_subscribers),
|
||||
]
|
||||
)
|
||||
|
||||
def _create_profile_pict(self, user: User):
|
||||
path = self.SAS_FIXTURE_PATH / "Family" / f"{user.username}.jpg"
|
||||
file = resize_image(Image.open(path), 400, "WEBP")
|
||||
|
||||
@@ -17,6 +17,7 @@ from counter.models import (
|
||||
Counter,
|
||||
Customer,
|
||||
Permanency,
|
||||
Price,
|
||||
Product,
|
||||
ProductType,
|
||||
Refilling,
|
||||
@@ -278,6 +279,7 @@ class Command(BaseCommand):
|
||||
# 2/3 of the products are owned by AE
|
||||
clubs = [ae, ae, ae, ae, ae, ae, *other_clubs]
|
||||
products = []
|
||||
prices = []
|
||||
buying_groups = []
|
||||
selling_places = []
|
||||
for _ in range(200):
|
||||
@@ -288,25 +290,28 @@ class Command(BaseCommand):
|
||||
product_type=random.choice(categories),
|
||||
code="".join(self.faker.random_letters(length=random.randint(4, 8))),
|
||||
purchase_price=price,
|
||||
selling_price=price,
|
||||
special_selling_price=price - min(0.5, price),
|
||||
club=random.choice(clubs),
|
||||
limit_age=0 if random.random() > 0.2 else 18,
|
||||
archived=bool(random.random() > 0.7),
|
||||
archived=self.faker.boolean(60),
|
||||
)
|
||||
products.append(product)
|
||||
# there will be products without buying groups
|
||||
# but there are also such products in the real database
|
||||
buying_groups.extend(
|
||||
Product.buying_groups.through(product=product, group=group)
|
||||
for group in random.sample(groups, k=random.randint(0, 3))
|
||||
for i in range(random.randint(0, 3)):
|
||||
product_price = Price(
|
||||
amount=price, product=product, is_always_shown=self.faker.boolean()
|
||||
)
|
||||
# prices for non-subscribers will be higher than for subscribers
|
||||
price *= 1.2
|
||||
prices.append(product_price)
|
||||
buying_groups.append(
|
||||
Price.groups.through(price=product_price, group=groups[i])
|
||||
)
|
||||
selling_places.extend(
|
||||
Counter.products.through(counter=counter, product=product)
|
||||
for counter in random.sample(counters, random.randint(0, 4))
|
||||
)
|
||||
Product.objects.bulk_create(products)
|
||||
Product.buying_groups.through.objects.bulk_create(buying_groups)
|
||||
Price.objects.bulk_create(prices)
|
||||
Price.groups.through.objects.bulk_create(buying_groups)
|
||||
Counter.products.through.objects.bulk_create(selling_places)
|
||||
|
||||
def create_sales(self, sellers: list[User]):
|
||||
@@ -320,7 +325,7 @@ class Command(BaseCommand):
|
||||
)
|
||||
)
|
||||
)
|
||||
products = list(Product.objects.all())
|
||||
prices = list(Price.objects.select_related("product").all())
|
||||
counters = list(
|
||||
Counter.objects.filter(name__in=["Foyer", "MDE", "La Gommette"])
|
||||
)
|
||||
@@ -330,14 +335,14 @@ class Command(BaseCommand):
|
||||
# the longer the customer has existed, the higher the mean of nb_products
|
||||
mu = 5 + (now().year - customer.since.year) * 2
|
||||
nb_sales = max(0, int(random.normalvariate(mu=mu, sigma=mu * 5)))
|
||||
favoured_products = random.sample(products, k=(random.randint(1, 5)))
|
||||
favoured_prices = random.sample(prices, k=(random.randint(1, 5)))
|
||||
favoured_counter = random.choice(counters)
|
||||
this_customer_sales = []
|
||||
for _ in range(nb_sales):
|
||||
product = (
|
||||
random.choice(favoured_products)
|
||||
price = (
|
||||
random.choice(favoured_prices)
|
||||
if random.random() > 0.7
|
||||
else random.choice(products)
|
||||
else random.choice(prices)
|
||||
)
|
||||
counter = (
|
||||
favoured_counter
|
||||
@@ -346,11 +351,11 @@ class Command(BaseCommand):
|
||||
)
|
||||
this_customer_sales.append(
|
||||
Selling(
|
||||
product=product,
|
||||
product=price.product,
|
||||
counter=counter,
|
||||
club_id=product.club_id,
|
||||
club_id=price.product.club_id,
|
||||
quantity=random.randint(1, 5),
|
||||
unit_price=product.selling_price,
|
||||
unit_price=price.amount,
|
||||
seller=random.choice(sellers),
|
||||
customer=customer,
|
||||
date=make_aware(
|
||||
|
||||
+7
-3
@@ -131,7 +131,9 @@ class UserQuerySet(models.QuerySet):
|
||||
if user.has_perm("core.view_hidden_user"):
|
||||
return self
|
||||
if user.has_perm("core.view_user"):
|
||||
return self.filter(Q(is_viewable=True) | Q(whitelisted_users=user))
|
||||
return self.filter(
|
||||
Q(is_viewable=True) | Q(whitelisted_users=user) | Q(pk=user.pk)
|
||||
)
|
||||
if user.is_anonymous:
|
||||
return self.none()
|
||||
return self.filter(id=user.id)
|
||||
@@ -884,8 +886,10 @@ class SithFile(models.Model):
|
||||
return self.get_parent_path() + "/" + self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
sas = SithFile.objects.filter(id=settings.SITH_SAS_ROOT_DIR_ID).first()
|
||||
self.is_in_sas = sas in self.get_parent_list() or self == sas
|
||||
sas_id = settings.SITH_SAS_ROOT_DIR_ID
|
||||
self.is_in_sas = self.id == sas_id or any(
|
||||
p.id == sas_id for p in self.get_parent_list()
|
||||
)
|
||||
adding = self._state.adding
|
||||
super().save(*args, **kwargs)
|
||||
if adding:
|
||||
|
||||
@@ -344,3 +344,14 @@ def test_quick_upload_image(
|
||||
assert (
|
||||
parsed["name"] == Path(file.name).stem[: QuickUploadImage.IMAGE_NAME_SIZE - 1]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_populated_sas_is_in_sas():
|
||||
"""Test that, in the data generated by the populate command,
|
||||
the SAS has value is_in_sas=True.
|
||||
|
||||
If it's not the case, it has no incidence in prod, but it's annoying
|
||||
in dev and may cause misunderstandings.
|
||||
"""
|
||||
assert SithFile.objects.get(id=settings.SITH_SAS_ROOT_DIR_ID).is_in_sas
|
||||
|
||||
+15
-7
@@ -213,9 +213,9 @@ def test_user_invoice_with_multiple_items():
|
||||
"""Test that annotate_total() works when invoices contain multiple items."""
|
||||
user: User = subscriber_user.make()
|
||||
item_recipe = Recipe(InvoiceItem, invoice=foreign_key(Recipe(Invoice, user=user)))
|
||||
item_recipe.make(_quantity=3, quantity=1, product_unit_price=5)
|
||||
item_recipe.make(_quantity=1, quantity=1, product_unit_price=5)
|
||||
item_recipe.make(_quantity=2, quantity=1, product_unit_price=iter([5, 8]))
|
||||
item_recipe.make(_quantity=3, quantity=1, unit_price=5)
|
||||
item_recipe.make(_quantity=1, quantity=1, unit_price=5)
|
||||
item_recipe.make(_quantity=2, quantity=1, unit_price=iter([5, 8]))
|
||||
res = list(
|
||||
Invoice.objects.filter(user=user)
|
||||
.annotate_total()
|
||||
@@ -410,12 +410,20 @@ class TestUserQuerySetViewableBy:
|
||||
assert set(viewable) == set(users)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user_factory", [old_subscriber_user.make, subscriber_user.make]
|
||||
"user_factory",
|
||||
[
|
||||
old_subscriber_user.make,
|
||||
lambda: old_subscriber_user.make(is_viewable=False),
|
||||
subscriber_user.make,
|
||||
lambda: subscriber_user.make(is_viewable=False),
|
||||
],
|
||||
)
|
||||
def test_subscriber(self, users: list[User], user_factory):
|
||||
def test_can_search(self, users: list[User], user_factory):
|
||||
user = user_factory()
|
||||
viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user)
|
||||
assert set(viewable) == {users[0], users[1]}
|
||||
viewable = User.objects.filter(
|
||||
id__in=[u.id for u in [*users, user]]
|
||||
).viewable_by(user)
|
||||
assert set(viewable) == {user, users[0], users[1]}
|
||||
|
||||
def test_whitelist(self, users: list[User]):
|
||||
user = subscriber_user.make()
|
||||
|
||||
+7
-1
@@ -24,6 +24,7 @@ from counter.models import (
|
||||
Eticket,
|
||||
InvoiceCall,
|
||||
Permanency,
|
||||
Price,
|
||||
Product,
|
||||
ProductType,
|
||||
Refilling,
|
||||
@@ -32,19 +33,24 @@ from counter.models import (
|
||||
)
|
||||
|
||||
|
||||
class PriceInline(admin.TabularInline):
|
||||
model = Price
|
||||
autocomplete_fields = ("groups",)
|
||||
|
||||
|
||||
@admin.register(Product)
|
||||
class ProductAdmin(SearchModelAdmin):
|
||||
list_display = (
|
||||
"name",
|
||||
"code",
|
||||
"product_type",
|
||||
"selling_price",
|
||||
"archived",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
list_select_related = ("product_type",)
|
||||
search_fields = ("name", "code")
|
||||
inlines = [PriceInline]
|
||||
|
||||
|
||||
@admin.register(ReturnableProduct)
|
||||
|
||||
+2
-6
@@ -101,13 +101,9 @@ class ProductController(ControllerBase):
|
||||
"""Get the detailed information about the products."""
|
||||
return filters.filter(
|
||||
Product.objects.select_related("club")
|
||||
.prefetch_related("buying_groups")
|
||||
.prefetch_related("prices", "prices__groups")
|
||||
.select_related("product_type")
|
||||
.order_by(
|
||||
F("product_type__order").asc(nulls_last=True),
|
||||
"product_type",
|
||||
"name",
|
||||
)
|
||||
.order_by(F("product_type__order").asc(nulls_last=True), "name")
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@ from model_bakery.recipe import Recipe, foreign_key
|
||||
|
||||
from club.models import Club
|
||||
from core.models import User
|
||||
from counter.models import Counter, Product, Refilling, Selling
|
||||
from counter.models import Counter, Price, Product, Refilling, Selling
|
||||
|
||||
counter_recipe = Recipe(Counter)
|
||||
product_recipe = Recipe(Product, club=foreign_key(Recipe(Club)))
|
||||
price_recipe = Recipe(Price, product=foreign_key(product_recipe))
|
||||
sale_recipe = Recipe(
|
||||
Selling,
|
||||
product=foreign_key(product_recipe),
|
||||
|
||||
+50
-62
@@ -1,12 +1,12 @@
|
||||
import json
|
||||
import math
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator
|
||||
from django.db.models import Exists, OuterRef, Q
|
||||
from django.forms import BaseModelFormSet
|
||||
from django.utils.timezone import now
|
||||
@@ -37,6 +37,7 @@ from counter.models import (
|
||||
Customer,
|
||||
Eticket,
|
||||
InvoiceCall,
|
||||
Price,
|
||||
Product,
|
||||
ProductFormula,
|
||||
Refilling,
|
||||
@@ -374,7 +375,21 @@ ScheduledProductActionFormSet = forms.modelformset_factory(
|
||||
can_delete=True,
|
||||
can_delete_extra=False,
|
||||
extra=0,
|
||||
)
|
||||
|
||||
|
||||
ProductPriceFormSet = forms.inlineformset_factory(
|
||||
parent_model=Product,
|
||||
model=Price,
|
||||
fields=["amount", "label", "groups", "is_always_shown"],
|
||||
widgets={
|
||||
"groups": AutoCompleteSelectMultipleGroup,
|
||||
"is_always_shown": forms.CheckboxInput(attrs={"class": "switch"}),
|
||||
},
|
||||
absolute_max=None,
|
||||
can_delete_extra=False,
|
||||
min_num=1,
|
||||
extra=0,
|
||||
)
|
||||
|
||||
|
||||
@@ -389,10 +404,7 @@ class ProductForm(forms.ModelForm):
|
||||
"description",
|
||||
"product_type",
|
||||
"code",
|
||||
"buying_groups",
|
||||
"purchase_price",
|
||||
"selling_price",
|
||||
"special_selling_price",
|
||||
"icon",
|
||||
"club",
|
||||
"limit_age",
|
||||
@@ -407,8 +419,8 @@ class ProductForm(forms.ModelForm):
|
||||
}
|
||||
widgets = {
|
||||
"product_type": AutoCompleteSelect,
|
||||
"buying_groups": AutoCompleteSelectMultipleGroup,
|
||||
"club": AutoCompleteSelectClub,
|
||||
"tray": forms.CheckboxInput(attrs={"class": "switch"}),
|
||||
}
|
||||
|
||||
counters = forms.ModelMultipleChoiceField(
|
||||
@@ -418,50 +430,40 @@ class ProductForm(forms.ModelForm):
|
||||
queryset=Counter.objects.all(),
|
||||
)
|
||||
|
||||
def __init__(self, *args, instance=None, **kwargs):
|
||||
super().__init__(*args, instance=instance, **kwargs)
|
||||
def __init__(self, *args, prefix: str | None = None, instance=None, **kwargs):
|
||||
super().__init__(*args, prefix=prefix, instance=instance, **kwargs)
|
||||
self.fields["name"].widget.attrs["autofocus"] = "autofocus"
|
||||
if self.instance.id:
|
||||
self.fields["counters"].initial = self.instance.counters.all()
|
||||
if hasattr(self.instance, "formula"):
|
||||
self.formula_init(self.instance.formula)
|
||||
self.price_formset = ProductPriceFormSet(
|
||||
*args, instance=self.instance, prefix="price", **kwargs
|
||||
)
|
||||
self.action_formset = ScheduledProductActionFormSet(
|
||||
*args, product=self.instance, **kwargs
|
||||
*args, product=self.instance, prefix="action", **kwargs
|
||||
)
|
||||
|
||||
def formula_init(self, formula: ProductFormula):
|
||||
"""Part of the form initialisation specific to formula products."""
|
||||
self.fields["selling_price"].help_text = _(
|
||||
"This product is a formula. "
|
||||
"Its price cannot be greater than the price "
|
||||
"of the products constituting it, which is %(price)s €"
|
||||
) % {"price": formula.max_selling_price}
|
||||
self.fields["special_selling_price"].help_text = _(
|
||||
"This product is a formula. "
|
||||
"Its special price cannot be greater than the price "
|
||||
"of the products constituting it, which is %(price)s €"
|
||||
) % {"price": formula.max_special_selling_price}
|
||||
for key, price in (
|
||||
("selling_price", formula.max_selling_price),
|
||||
("special_selling_price", formula.max_special_selling_price),
|
||||
):
|
||||
self.fields[key].widget.attrs["max"] = price
|
||||
self.fields[key].validators.append(MaxValueValidator(price))
|
||||
|
||||
def is_valid(self):
|
||||
return super().is_valid() and self.action_formset.is_valid()
|
||||
return (
|
||||
super().is_valid()
|
||||
and self.price_formset.is_valid()
|
||||
and self.action_formset.is_valid()
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs) -> Product:
|
||||
product = super().save(*args, **kwargs)
|
||||
product.counters.set(self.cleaned_data["counters"])
|
||||
for form in self.action_formset:
|
||||
# if it's a creation, the product given in the formset
|
||||
# wasn't a persisted instance.
|
||||
# So if we tried to persist the scheduled actions in the current state,
|
||||
# So if we tried to persist the related objects in the current state,
|
||||
# they would be linked to no product, thus be completely useless
|
||||
# To make it work, we have to replace
|
||||
# the initial product with a persisted one
|
||||
for form in self.action_formset:
|
||||
form.set_product(product)
|
||||
self.action_formset.save()
|
||||
self.price_formset.save()
|
||||
return product
|
||||
|
||||
|
||||
@@ -484,18 +486,6 @@ class ProductFormulaForm(forms.ModelForm):
|
||||
"the result and a part of the formula."
|
||||
),
|
||||
)
|
||||
prices = [p.selling_price for p in cleaned_data["products"]]
|
||||
special_prices = [p.special_selling_price for p in cleaned_data["products"]]
|
||||
selling_price = cleaned_data["result"].selling_price
|
||||
special_selling_price = cleaned_data["result"].special_selling_price
|
||||
if selling_price > sum(prices) or special_selling_price > sum(special_prices):
|
||||
self.add_error(
|
||||
"result",
|
||||
_(
|
||||
"The result cannot be more expensive "
|
||||
"than the total of the other products."
|
||||
),
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
|
||||
@@ -546,48 +536,47 @@ class CloseCustomerAccountForm(forms.Form):
|
||||
)
|
||||
|
||||
|
||||
class BasketProductForm(forms.Form):
|
||||
class BasketItemForm(forms.Form):
|
||||
quantity = forms.IntegerField(min_value=1, required=True)
|
||||
id = forms.IntegerField(min_value=0, required=True)
|
||||
price_id = forms.IntegerField(min_value=0, required=True)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
customer: Customer,
|
||||
counter: Counter,
|
||||
allowed_products: dict[int, Product],
|
||||
allowed_prices: dict[int, Price],
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.customer = customer # Used by formset
|
||||
self.counter = counter # Used by formset
|
||||
self.allowed_products = allowed_products
|
||||
self.allowed_prices = allowed_prices
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def clean_id(self):
|
||||
data = self.cleaned_data["id"]
|
||||
def clean_price_id(self):
|
||||
data = self.cleaned_data["price_id"]
|
||||
|
||||
# We store self.product so we can use it later on the formset validation
|
||||
# We store self.price so we can use it later on the formset validation
|
||||
# And also in the global clean
|
||||
self.product = self.allowed_products.get(data, None)
|
||||
if self.product is None:
|
||||
self.price = self.allowed_prices.get(data, None)
|
||||
if self.price is None:
|
||||
raise forms.ValidationError(
|
||||
_("The selected product isn't available for this user")
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
if len(self.errors) > 0:
|
||||
return
|
||||
return cleaned_data
|
||||
|
||||
# Compute prices
|
||||
cleaned_data["bonus_quantity"] = 0
|
||||
if self.product.tray:
|
||||
if self.price.product.tray:
|
||||
cleaned_data["bonus_quantity"] = math.floor(
|
||||
cleaned_data["quantity"] / Product.QUANTITY_FOR_TRAY_PRICE
|
||||
)
|
||||
cleaned_data["total_price"] = self.product.price * (
|
||||
cleaned_data["total_price"] = self.price.amount * (
|
||||
cleaned_data["quantity"] - cleaned_data["bonus_quantity"]
|
||||
)
|
||||
|
||||
@@ -611,8 +600,8 @@ class BaseBasketForm(forms.BaseFormSet):
|
||||
raise forms.ValidationError(_("Submitted basket is invalid"))
|
||||
|
||||
def _check_product_are_unique(self):
|
||||
product_ids = {form.cleaned_data["id"] for form in self.forms}
|
||||
if len(product_ids) != len(self.forms):
|
||||
price_ids = {form.cleaned_data["price_id"] for form in self.forms}
|
||||
if len(price_ids) != len(self.forms):
|
||||
raise forms.ValidationError(_("Duplicated product entries."))
|
||||
|
||||
def _check_enough_money(self, counter: Counter, customer: Customer):
|
||||
@@ -622,10 +611,9 @@ class BaseBasketForm(forms.BaseFormSet):
|
||||
|
||||
def _check_recorded_products(self, customer: Customer):
|
||||
"""Check for, among other things, ecocups and pitchers"""
|
||||
items = {
|
||||
form.cleaned_data["id"]: form.cleaned_data["quantity"]
|
||||
for form in self.forms
|
||||
}
|
||||
items = defaultdict(int)
|
||||
for form in self.forms:
|
||||
items[form.price.product_id] += form.cleaned_data["quantity"]
|
||||
ids = list(items.keys())
|
||||
returnables = list(
|
||||
ReturnableProduct.objects.filter(
|
||||
@@ -651,7 +639,7 @@ class BaseBasketForm(forms.BaseFormSet):
|
||||
|
||||
|
||||
BasketForm = forms.formset_factory(
|
||||
BasketProductForm, formset=BaseBasketForm, absolute_max=None, min_num=1
|
||||
BasketItemForm, formset=BaseBasketForm, absolute_max=None, min_num=1
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
# Generated by Django 5.2.11 on 2026-02-18 13:30
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
from django.db.migrations.state import StateApps
|
||||
|
||||
import counter.fields
|
||||
|
||||
|
||||
def migrate_prices(apps: StateApps, schema_editor):
|
||||
Product = apps.get_model("counter", "Product")
|
||||
Price = apps.get_model("counter", "Price")
|
||||
prices = [
|
||||
Price(
|
||||
amount=p.selling_price,
|
||||
product=p,
|
||||
created_at=p.created_at,
|
||||
updated_at=p.updated_at,
|
||||
)
|
||||
for p in Product.objects.all()
|
||||
]
|
||||
Price.objects.bulk_create(prices)
|
||||
groups = [
|
||||
Price.groups.through(price=price, group=group)
|
||||
for price in Price.objects.select_related("product").prefetch_related(
|
||||
"product__buying_groups"
|
||||
)
|
||||
for group in price.product.buying_groups.all()
|
||||
]
|
||||
Price.groups.through.objects.bulk_create(groups)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("core", "0048_alter_user_options"),
|
||||
("counter", "0038_countersellers"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Price",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"amount",
|
||||
counter.fields.CurrencyField(
|
||||
decimal_places=2, max_digits=12, verbose_name="amount"
|
||||
),
|
||||
),
|
||||
(
|
||||
"is_always_shown",
|
||||
models.BooleanField(
|
||||
default=False,
|
||||
help_text=(
|
||||
"If this option is enabled, "
|
||||
"people will see this price and be able to pay it, "
|
||||
"even if another cheaper price exists. "
|
||||
"Else it will visible only if it is the cheapest available price."
|
||||
),
|
||||
verbose_name="always show",
|
||||
),
|
||||
),
|
||||
(
|
||||
"label",
|
||||
models.CharField(
|
||||
default="",
|
||||
help_text=(
|
||||
"A short label for easier differentiation "
|
||||
"if a user can see multiple prices."
|
||||
),
|
||||
max_length=32,
|
||||
verbose_name="label",
|
||||
blank=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
"created_at",
|
||||
models.DateTimeField(auto_now_add=True, verbose_name="created at"),
|
||||
),
|
||||
(
|
||||
"updated_at",
|
||||
models.DateTimeField(auto_now=True, verbose_name="updated at"),
|
||||
),
|
||||
(
|
||||
"groups",
|
||||
models.ManyToManyField(
|
||||
related_name="prices", to="core.group", verbose_name="groups"
|
||||
),
|
||||
),
|
||||
(
|
||||
"product",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="prices",
|
||||
to="counter.product",
|
||||
verbose_name="product",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={"verbose_name": "price"},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="product",
|
||||
name="tray",
|
||||
field=models.BooleanField(
|
||||
default=False,
|
||||
help_text="Buy five, get the sixth free",
|
||||
verbose_name="tray price",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(migrate_prices, reverse_code=migrations.RunPython.noop),
|
||||
migrations.RemoveField(model_name="product", name="selling_price"),
|
||||
migrations.RemoveField(model_name="product", name="special_selling_price"),
|
||||
migrations.AlterField(
|
||||
model_name="product",
|
||||
name="description",
|
||||
field=models.TextField(blank=True, default="", verbose_name="description"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="product",
|
||||
name="product_type",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="products",
|
||||
to="counter.producttype",
|
||||
verbose_name="product type",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="productformula",
|
||||
name="result",
|
||||
field=models.OneToOneField(
|
||||
help_text="The product got with the formula.",
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="formula",
|
||||
to="counter.product",
|
||||
verbose_name="result product",
|
||||
),
|
||||
),
|
||||
]
|
||||
+91
-87
@@ -22,7 +22,7 @@ import string
|
||||
from datetime import date, datetime, timedelta
|
||||
from datetime import timezone as tz
|
||||
from decimal import Decimal
|
||||
from typing import Literal, Self
|
||||
from typing import TYPE_CHECKING, Literal, Self
|
||||
|
||||
from dict2xml import dict2xml
|
||||
from django.conf import settings
|
||||
@@ -47,6 +47,9 @@ from core.utils import get_start_of_semester
|
||||
from counter.fields import CurrencyField
|
||||
from subscription.models import Subscription
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
|
||||
def get_eboutic() -> Counter:
|
||||
return Counter.objects.filter(type="EBOUTIC").order_by("id").first()
|
||||
@@ -157,14 +160,7 @@ class Customer(models.Model):
|
||||
|
||||
@property
|
||||
def can_buy(self) -> bool:
|
||||
"""Check if whether this customer has the right to purchase any item.
|
||||
|
||||
This must be not confused with the Product.can_be_sold_to(user)
|
||||
method as the present method returns an information
|
||||
about a customer whereas the other tells something
|
||||
about the relation between a User (not a Customer,
|
||||
don't mix them) and a Product.
|
||||
"""
|
||||
"""Check if whether this customer has the right to purchase any item."""
|
||||
subscription = self.user.subscriptions.order_by("subscription_end").last()
|
||||
if subscription is None:
|
||||
return False
|
||||
@@ -363,13 +359,13 @@ class Product(models.Model):
|
||||
QUANTITY_FOR_TRAY_PRICE = 6
|
||||
|
||||
name = models.CharField(_("name"), max_length=64)
|
||||
description = models.TextField(_("description"), default="")
|
||||
description = models.TextField(_("description"), blank=True, default="")
|
||||
product_type = models.ForeignKey(
|
||||
ProductType,
|
||||
related_name="products",
|
||||
verbose_name=_("product type"),
|
||||
null=True,
|
||||
blank=True,
|
||||
blank=False,
|
||||
on_delete=models.SET_NULL,
|
||||
)
|
||||
code = models.CharField(_("code"), max_length=16, blank=True)
|
||||
@@ -377,11 +373,6 @@ class Product(models.Model):
|
||||
_("purchase price"),
|
||||
help_text=_("Initial cost of purchasing the product"),
|
||||
)
|
||||
selling_price = CurrencyField(_("selling price"))
|
||||
special_selling_price = CurrencyField(
|
||||
_("special selling price"),
|
||||
help_text=_("Price for barmen during their permanence"),
|
||||
)
|
||||
icon = ResizedImageField(
|
||||
height=70,
|
||||
force_format="WEBP",
|
||||
@@ -394,7 +385,9 @@ class Product(models.Model):
|
||||
Club, related_name="products", verbose_name=_("club"), on_delete=models.CASCADE
|
||||
)
|
||||
limit_age = models.IntegerField(_("limit age"), default=0)
|
||||
tray = models.BooleanField(_("tray price"), default=False)
|
||||
tray = models.BooleanField(
|
||||
_("tray price"), help_text=_("Buy five, get the sixth free"), default=False
|
||||
)
|
||||
buying_groups = models.ManyToManyField(
|
||||
Group, related_name="products", verbose_name=_("buying groups"), blank=True
|
||||
)
|
||||
@@ -419,41 +412,77 @@ class Product(models.Model):
|
||||
pk=settings.SITH_GROUP_ACCOUNTING_ADMIN_ID
|
||||
) or user.is_in_group(pk=settings.SITH_GROUP_COUNTER_ADMIN_ID)
|
||||
|
||||
def can_be_sold_to(self, user: User) -> bool:
|
||||
"""Check if whether the user given in parameter has the right to buy
|
||||
this product or not.
|
||||
|
||||
This must be not confused with the Customer.can_buy()
|
||||
method as the present method returns an information
|
||||
about the relation between a User and a Product,
|
||||
whereas the other tells something about a Customer
|
||||
(and not a user, they are not the same model).
|
||||
class PriceQuerySet(models.QuerySet):
|
||||
def for_user(self, user: User) -> Self:
|
||||
age = user.age
|
||||
if user.is_banned_alcohol:
|
||||
age = min(age, 17)
|
||||
return self.filter(
|
||||
Q(is_always_shown=True, groups__in=user.all_groups)
|
||||
| Q(
|
||||
id=Subquery(
|
||||
Price.objects.filter(
|
||||
product_id=OuterRef("product_id"), groups__in=user.all_groups
|
||||
)
|
||||
.order_by("amount")
|
||||
.values("id")[:1]
|
||||
)
|
||||
),
|
||||
product__archived=False,
|
||||
product__limit_age__lte=age,
|
||||
)
|
||||
|
||||
Returns:
|
||||
True if the user can buy this product else False
|
||||
|
||||
Warning:
|
||||
This performs a db query, thus you can quickly have
|
||||
a N+1 queries problem if you call it in a loop.
|
||||
Hopefully, you can avoid that if you prefetch the buying_groups :
|
||||
class Price(models.Model):
|
||||
amount = CurrencyField(_("amount"))
|
||||
product = models.ForeignKey(
|
||||
Product,
|
||||
verbose_name=_("product"),
|
||||
related_name="prices",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
groups = models.ManyToManyField(
|
||||
Group, verbose_name=_("groups"), related_name="prices"
|
||||
)
|
||||
is_always_shown = models.BooleanField(
|
||||
_("always show"),
|
||||
help_text=_(
|
||||
"If this option is enabled, "
|
||||
"people will see this price and be able to pay it, "
|
||||
"even if another cheaper price exists. "
|
||||
"Else it will visible only if it is the cheapest available price."
|
||||
),
|
||||
default=False,
|
||||
)
|
||||
label = models.CharField(
|
||||
_("label"),
|
||||
help_text=_(
|
||||
"A short label for easier differentiation "
|
||||
"if a user can see multiple prices."
|
||||
),
|
||||
max_length=32,
|
||||
default="",
|
||||
blank=True,
|
||||
)
|
||||
created_at = models.DateTimeField(_("created at"), auto_now_add=True)
|
||||
updated_at = models.DateTimeField(_("updated at"), auto_now=True)
|
||||
|
||||
```python
|
||||
user = User.objects.get(username="foobar")
|
||||
products = [
|
||||
p
|
||||
for p in Product.objects.prefetch_related("buying_groups")
|
||||
if p.can_be_sold_to(user)
|
||||
]
|
||||
```
|
||||
"""
|
||||
buying_groups = list(self.buying_groups.all())
|
||||
if not buying_groups:
|
||||
return True
|
||||
return any(user.is_in_group(pk=group.id) for group in buying_groups)
|
||||
objects = PriceQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("price")
|
||||
|
||||
def __str__(self):
|
||||
if not self.label:
|
||||
return f"{self.product.name} ({self.amount}€)"
|
||||
return f"{self.product.name} {self.label} ({self.amount}€)"
|
||||
|
||||
@property
|
||||
def profit(self):
|
||||
return self.selling_price - self.purchase_price
|
||||
def full_label(self):
|
||||
if not self.label:
|
||||
return self.product.name
|
||||
return f"{self.product.name} \u2013 {self.label}"
|
||||
|
||||
|
||||
class ProductFormula(models.Model):
|
||||
@@ -474,18 +503,6 @@ class ProductFormula(models.Model):
|
||||
def __str__(self):
|
||||
return self.result.name
|
||||
|
||||
@cached_property
|
||||
def max_selling_price(self) -> float:
|
||||
# iterating over all products is less efficient than doing
|
||||
# a simple aggregation, but this method is likely to be used in
|
||||
# coordination with `max_special_selling_price`,
|
||||
# and Django caches the result of the `all` queryset.
|
||||
return sum(p.selling_price for p in self.products.all())
|
||||
|
||||
@cached_property
|
||||
def max_special_selling_price(self) -> float:
|
||||
return sum(p.special_selling_price for p in self.products.all())
|
||||
|
||||
|
||||
class CounterQuerySet(models.QuerySet):
|
||||
def annotate_has_barman(self, user: User) -> Self:
|
||||
@@ -716,35 +733,20 @@ class Counter(models.Model):
|
||||
# but they share the same primary key
|
||||
return self.type == "BAR" and any(b.pk == customer.pk for b in self.barmen_list)
|
||||
|
||||
def get_products_for(self, customer: Customer) -> list[Product]:
|
||||
"""
|
||||
Get all allowed products for the provided customer on this counter
|
||||
Prices will be annotated
|
||||
"""
|
||||
|
||||
products = (
|
||||
self.products.filter(archived=False)
|
||||
.select_related("product_type")
|
||||
.prefetch_related("buying_groups")
|
||||
def get_prices_for(
|
||||
self, customer: Customer, *, order_by: Sequence[str] | None = None
|
||||
) -> list[Price]:
|
||||
qs = (
|
||||
Price.objects.filter(
|
||||
product__counters=self, product__product_type__isnull=False
|
||||
)
|
||||
|
||||
# Only include age appropriate products
|
||||
age = customer.user.age
|
||||
if customer.user.is_banned_alcohol:
|
||||
age = min(age, 17)
|
||||
products = products.filter(limit_age__lte=age)
|
||||
|
||||
# Compute special price for customer if he is a barmen on that bar
|
||||
if self.customer_is_barman(customer):
|
||||
products = products.annotate(price=F("special_selling_price"))
|
||||
else:
|
||||
products = products.annotate(price=F("selling_price"))
|
||||
|
||||
return [
|
||||
product
|
||||
for product in products.all()
|
||||
if product.can_be_sold_to(customer.user)
|
||||
]
|
||||
.for_user(customer.user)
|
||||
.select_related("product", "product__product_type")
|
||||
.prefetch_related("groups")
|
||||
)
|
||||
if order_by:
|
||||
qs = qs.order_by(*order_by)
|
||||
return list(qs)
|
||||
|
||||
|
||||
class CounterSellers(models.Model):
|
||||
@@ -1025,7 +1027,9 @@ class Selling(models.Model):
|
||||
event = self.product.eticket.event_title or _("Unknown event")
|
||||
subject = _("Eticket bought for the event %(event)s") % {"event": event}
|
||||
message_html = _(
|
||||
"You bought an eticket for the event %(event)s.\nYou can download it directly from this link %(eticket)s.\nYou can also retrieve all your e-tickets on your account page %(url)s."
|
||||
"You bought an eticket for the event %(event)s.\n"
|
||||
"You can download it directly from this link %(eticket)s.\n"
|
||||
"You can also retrieve all your e-tickets on your account page %(url)s."
|
||||
) % {
|
||||
"event": event,
|
||||
"url": (
|
||||
|
||||
+9
-4
@@ -6,8 +6,8 @@ from ninja import FilterLookup, FilterSchema, ModelSchema, Schema
|
||||
from pydantic import model_validator
|
||||
|
||||
from club.schemas import SimpleClubSchema
|
||||
from core.schemas import GroupSchema, NonEmptyStr, SimpleUserSchema
|
||||
from counter.models import Counter, Product, ProductType
|
||||
from core.schemas import NonEmptyStr, SimpleUserSchema
|
||||
from counter.models import Counter, Price, Product, ProductType
|
||||
|
||||
|
||||
class CounterSchema(ModelSchema):
|
||||
@@ -66,6 +66,12 @@ class SimpleProductSchema(ModelSchema):
|
||||
fields = ["id", "name", "code"]
|
||||
|
||||
|
||||
class ProductPriceSchema(ModelSchema):
|
||||
class Meta:
|
||||
model = Price
|
||||
fields = ["amount", "groups"]
|
||||
|
||||
|
||||
class ProductSchema(ModelSchema):
|
||||
class Meta:
|
||||
model = Product
|
||||
@@ -75,13 +81,12 @@ class ProductSchema(ModelSchema):
|
||||
"code",
|
||||
"description",
|
||||
"purchase_price",
|
||||
"selling_price",
|
||||
"icon",
|
||||
"limit_age",
|
||||
"archived",
|
||||
]
|
||||
|
||||
buying_groups: list[GroupSchema]
|
||||
prices: list[ProductPriceSchema]
|
||||
club: SimpleClubSchema
|
||||
product_type: SimpleProductTypeSchema | None
|
||||
url: str
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { Product } from "#counter:counter/types.ts";
|
||||
import type { CounterItem } from "#counter:counter/types";
|
||||
|
||||
export class BasketItem {
|
||||
quantity: number;
|
||||
product: Product;
|
||||
quantityForTrayPrice: number;
|
||||
product: CounterItem;
|
||||
errors: string[];
|
||||
|
||||
constructor(product: Product, quantity: number) {
|
||||
constructor(product: CounterItem, quantity: number) {
|
||||
this.quantity = quantity;
|
||||
this.product = product;
|
||||
this.errors = [];
|
||||
@@ -20,6 +19,6 @@ export class BasketItem {
|
||||
}
|
||||
|
||||
sum(): number {
|
||||
return (this.quantity - this.getBonusQuantity()) * this.product.price;
|
||||
return (this.quantity - this.getBonusQuantity()) * this.product.price.amount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { AlertMessage } from "#core:utils/alert-message.ts";
|
||||
import { BasketItem } from "#counter:counter/basket.ts";
|
||||
import { AlertMessage } from "#core:utils/alert-message";
|
||||
import { BasketItem } from "#counter:counter/basket";
|
||||
import type {
|
||||
CounterConfig,
|
||||
CounterItem,
|
||||
ErrorMessage,
|
||||
ProductFormula,
|
||||
} from "#counter:counter/types.ts";
|
||||
import type { CounterProductSelect } from "./components/counter-product-select-index.ts";
|
||||
} from "#counter:counter/types";
|
||||
import type { CounterProductSelect } from "./components/counter-product-select-index";
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("counter", (config: CounterConfig) => ({
|
||||
@@ -63,8 +64,10 @@ document.addEventListener("alpine:init", () => {
|
||||
},
|
||||
|
||||
checkFormulas() {
|
||||
// Try to find a formula.
|
||||
// A formula is found if all its elements are already in the basket
|
||||
const products = new Set(
|
||||
Object.keys(this.basket).map((i: string) => Number.parseInt(i, 10)),
|
||||
Object.values(this.basket).map((item: BasketItem) => item.product.productId),
|
||||
);
|
||||
const formula: ProductFormula = config.formulas.find((f: ProductFormula) => {
|
||||
return f.products.every((p: number) => products.has(p));
|
||||
@@ -72,22 +75,29 @@ document.addEventListener("alpine:init", () => {
|
||||
if (formula === undefined) {
|
||||
return;
|
||||
}
|
||||
// Now that the formula is found, remove the items composing it from the basket
|
||||
for (const product of formula.products) {
|
||||
const key = product.toString();
|
||||
const key = Object.entries(this.basket).find(
|
||||
([_, i]: [string, BasketItem]) => i.product.productId === product,
|
||||
)[0];
|
||||
this.basket[key].quantity -= 1;
|
||||
if (this.basket[key].quantity <= 0) {
|
||||
this.removeFromBasket(key);
|
||||
}
|
||||
}
|
||||
// Then add the result product of the formula to the basket
|
||||
const result = Object.values(config.products)
|
||||
.filter((item: CounterItem) => item.productId === formula.result)
|
||||
.reduce((acc, curr) => (acc.price.amount < curr.price.amount ? acc : curr));
|
||||
this.addToBasket(result.price.id, 1);
|
||||
this.alertMessage.display(
|
||||
interpolate(
|
||||
gettext("Formula %(formula)s applied"),
|
||||
{ formula: config.products[formula.result.toString()].name },
|
||||
{ formula: result.name },
|
||||
true,
|
||||
),
|
||||
{ success: true },
|
||||
);
|
||||
this.addToBasket(formula.result.toString(), 1);
|
||||
},
|
||||
|
||||
getBasketSize() {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { showSaveFilePicker } from "native-file-system-adapter";
|
||||
import type TomSelect from "tom-select";
|
||||
import { paginated } from "#core:utils/api.ts";
|
||||
import { csv } from "#core:utils/csv.ts";
|
||||
import {
|
||||
getCurrentUrlParams,
|
||||
History,
|
||||
updateQueryString,
|
||||
} from "#core:utils/history.ts";
|
||||
import type { NestedKeyOf } from "#core:utils/types.ts";
|
||||
import { paginated } from "#core:utils/api";
|
||||
import { csv } from "#core:utils/csv";
|
||||
import { getCurrentUrlParams, History, updateQueryString } from "#core:utils/history";
|
||||
import type { NestedKeyOf } from "#core:utils/types";
|
||||
import {
|
||||
type ProductSchema,
|
||||
type ProductSearchProductsDetailedData,
|
||||
@@ -20,6 +16,9 @@ type GroupedProducts = Record<ProductType, ProductSchema[]>;
|
||||
const defaultPageSize = 100;
|
||||
const defaultPage = 1;
|
||||
|
||||
// biome-ignore lint/style/useNamingConvention: api is snake case
|
||||
type ProductWithPriceSchema = ProductSchema & { selling_price: string };
|
||||
|
||||
/**
|
||||
* Keys of the properties to include in the CSV.
|
||||
*/
|
||||
@@ -34,7 +33,7 @@ const csvColumns = [
|
||||
"purchase_price",
|
||||
"selling_price",
|
||||
"archived",
|
||||
] as NestedKeyOf<ProductSchema>[];
|
||||
] as NestedKeyOf<ProductWithPriceSchema>[];
|
||||
|
||||
/**
|
||||
* Title of the csv columns.
|
||||
@@ -175,7 +174,16 @@ document.addEventListener("alpine:init", () => {
|
||||
this.nbPages > 1
|
||||
? await paginated(productSearchProductsDetailed, this.getQueryParams())
|
||||
: Object.values<ProductSchema[]>(this.products).flat();
|
||||
const content = csv.stringify(products, {
|
||||
// CSV cannot represent nested data
|
||||
// so we create a row for each price of each product.
|
||||
const productsWithPrice: ProductWithPriceSchema[] = products.flatMap(
|
||||
(product: ProductSchema) =>
|
||||
product.prices.map((price) =>
|
||||
// biome-ignore lint/style/useNamingConvention: API is snake_case
|
||||
Object.assign(product, { selling_price: price.amount }),
|
||||
),
|
||||
);
|
||||
const content = csv.stringify(productsWithPrice, {
|
||||
columns: csvColumns,
|
||||
titleRow: csvColumnTitles,
|
||||
});
|
||||
|
||||
+10
-5
@@ -2,7 +2,7 @@ export type ErrorMessage = string;
|
||||
|
||||
export interface InitialFormData {
|
||||
/* Used to refill the form when the backend raises an error */
|
||||
id?: keyof Record<string, Product>;
|
||||
id?: keyof Record<string, CounterItem>;
|
||||
quantity?: number;
|
||||
errors?: string[];
|
||||
}
|
||||
@@ -15,17 +15,22 @@ export interface ProductFormula {
|
||||
export interface CounterConfig {
|
||||
customerBalance: number;
|
||||
customerId: number;
|
||||
products: Record<string, Product>;
|
||||
products: Record<string, CounterItem>;
|
||||
formulas: ProductFormula[];
|
||||
formInitial: InitialFormData[];
|
||||
cancelUrl: string;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
interface Price {
|
||||
id: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface CounterItem {
|
||||
productId: number;
|
||||
price: Price;
|
||||
code: string;
|
||||
name: string;
|
||||
price: number;
|
||||
hasTrayPrice: boolean;
|
||||
quantityForTrayPrice: number;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_css %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ static('counter/css/counter-click.scss') }}" defer></link>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static('bundled/core/components/ajax-select-index.css') }}" defer></link>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static('core/components/ajax-select.scss') }}" defer></link>
|
||||
<link rel="stylesheet" type="text/css" href="{{ static('core/components/tabs.scss') }}" defer></link>
|
||||
<link rel="stylesheet" href="{{ static('counter/css/counter-click.scss') }}">
|
||||
<link rel="stylesheet" href="{{ static('bundled/core/components/ajax-select-index.css') }}">
|
||||
<link rel="stylesheet" href="{{ static('core/components/ajax-select.scss') }}">
|
||||
<link rel="stylesheet" href="{{ static('core/components/tabs.scss') }}">
|
||||
<link rel="stylesheet" href="{{ static("core/components/card.scss") }}">
|
||||
{% endblock %}
|
||||
|
||||
@@ -65,10 +65,10 @@
|
||||
<option value="FIN">{% trans %}Confirm (FIN){% endtrans %}</option>
|
||||
<option value="ANN">{% trans %}Cancel (ANN){% endtrans %}</option>
|
||||
</optgroup>
|
||||
{%- for category in categories.keys() -%}
|
||||
{%- for category, prices in categories.items() -%}
|
||||
<optgroup label="{{ category }}">
|
||||
{%- for product in categories[category] -%}
|
||||
<option value="{{ product.id }}">{{ product }}</option>
|
||||
{%- for price in prices -%}
|
||||
<option value="{{ price.id }}">{{ price.full_label }}</option>
|
||||
{%- endfor -%}
|
||||
</optgroup>
|
||||
{%- endfor -%}
|
||||
@@ -103,24 +103,25 @@
|
||||
</div>
|
||||
<ul>
|
||||
<li x-show="getBasketSize() === 0">{% trans %}This basket is empty{% endtrans %}</li>
|
||||
<template x-for="(item, index) in Object.values(basket)" :key="item.product.id">
|
||||
<template x-for="(item, index) in Object.values(basket)" :key="item.product.price.id">
|
||||
<li>
|
||||
<template x-for="error in item.errors">
|
||||
<div class="alert alert-red" x-text="error">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<button @click.prevent="addToBasket(item.product.id, -1)">-</button>
|
||||
<button @click.prevent="addToBasket(item.product.price.id, -1)">-</button>
|
||||
<span class="quantity" x-text="item.quantity"></span>
|
||||
<button @click.prevent="addToBasket(item.product.id, 1)">+</button>
|
||||
<button @click.prevent="addToBasket(item.product.price.id, 1)">+</button>
|
||||
|
||||
<span x-text="item.product.name"></span> :
|
||||
<span x-text="item.sum().toLocaleString(undefined, { minimumFractionDigits: 2 })">€</span>
|
||||
<span x-show="item.getBonusQuantity() > 0" x-text="`${item.getBonusQuantity()} x P`"></span>
|
||||
<span x-show="item.getBonusQuantity() > 0"
|
||||
x-text="`${item.getBonusQuantity()} x P`"></span>
|
||||
|
||||
<button
|
||||
class="remove-item"
|
||||
@click.prevent="removeFromBasket(item.product.id)"
|
||||
@click.prevent="removeFromBasket(item.product.price.id)"
|
||||
><i class="fa fa-trash-can delete-action"></i></button>
|
||||
|
||||
<input
|
||||
@@ -133,9 +134,9 @@
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
:value="item.product.id"
|
||||
:id="`id_form-${index}-id`"
|
||||
:name="`form-${index}-id`"
|
||||
:value="item.product.price.id"
|
||||
:id="`id_form-${index}-price_id`"
|
||||
:name="`form-${index}-price_id`"
|
||||
required
|
||||
readonly
|
||||
>
|
||||
@@ -201,30 +202,30 @@
|
||||
</div>
|
||||
|
||||
<div id="products">
|
||||
{% if not products %}
|
||||
{% if not prices %}
|
||||
<div class="alert alert-red">
|
||||
{% trans %}No products available on this counter for this user{% endtrans %}
|
||||
</div>
|
||||
{% else %}
|
||||
<ui-tab-group>
|
||||
{% for category in categories.keys() -%}
|
||||
{% for category, prices in categories.items() -%}
|
||||
<ui-tab title="{{ category }}" {% if loop.index == 1 -%}active{%- endif -%}>
|
||||
<h5 class="margin-bottom">{{ category }}</h5>
|
||||
<div class="row gap-2x">
|
||||
{% for product in categories[category] -%}
|
||||
<button class="card shadow" @click="addToBasket('{{ product.id }}', 1)">
|
||||
{% for price in prices -%}
|
||||
<button class="card shadow" @click="addToBasket('{{ price.id }}', 1)">
|
||||
<img
|
||||
class="card-image"
|
||||
alt="image de {{ product.name }}"
|
||||
{% if product.icon %}
|
||||
src="{{ product.icon.url }}"
|
||||
alt="image de {{ price.full_label }}"
|
||||
{% if price.product.icon %}
|
||||
src="{{ price.product.icon.url }}"
|
||||
{% else %}
|
||||
src="{{ static('core/img/na.gif') }}"
|
||||
{% endif %}
|
||||
/>
|
||||
<span class="card-content">
|
||||
<strong class="card-title">{{ product.name }}</strong>
|
||||
<p>{{ product.price }} €<br>{{ product.code }}</p>
|
||||
<strong class="card-title">{{ price.full_label }}</strong>
|
||||
<p>{{ price.amount }} €<br>{{ price.product.code }}</p>
|
||||
</span>
|
||||
</button>
|
||||
{%- endfor %}
|
||||
@@ -241,13 +242,14 @@
|
||||
{{ super() }}
|
||||
<script>
|
||||
const products = {
|
||||
{%- for product in products -%}
|
||||
{{ product.id }}: {
|
||||
id: "{{ product.id }}",
|
||||
name: "{{ product.name }}",
|
||||
price: {{ product.price }},
|
||||
hasTrayPrice: {{ product.tray | tojson }},
|
||||
quantityForTrayPrice: {{ product.QUANTITY_FOR_TRAY_PRICE }},
|
||||
{%- for price in prices -%}
|
||||
{{ price.id }}: {
|
||||
productId: {{ price.product_id }},
|
||||
price: { id: "{{ price.id }}", amount: {{ price.amount }} },
|
||||
code: "{{ price.product.code }}",
|
||||
name: "{{ price.full_label }}",
|
||||
hasTrayPrice: {{ price.product.tray | tojson }},
|
||||
quantityForTrayPrice: {{ price.product.QUANTITY_FOR_TRAY_PRICE }},
|
||||
},
|
||||
{%- endfor -%}
|
||||
};
|
||||
|
||||
@@ -49,14 +49,10 @@
|
||||
<strong class="card-title">{{ formula.result.name }}</strong>
|
||||
<p>
|
||||
{% for p in formula.products.all() %}
|
||||
<i>{{ p.code }} ({{ p.selling_price }} €)</i>
|
||||
<i>{{ p.name }} ({{ p.code }})</i>
|
||||
{% if not loop.last %}+{% endif %}
|
||||
{% endfor %}
|
||||
</p>
|
||||
<p>
|
||||
{{ formula.result.selling_price }} €
|
||||
({% trans %}instead of{% endtrans %} {{ formula.max_selling_price}} €)
|
||||
</p>
|
||||
</div>
|
||||
{% if user.has_perm("counter.delete_productformula") %}
|
||||
<button
|
||||
|
||||
@@ -39,6 +39,49 @@
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% macro price_form(form) %}
|
||||
<fieldset>
|
||||
{{ form.non_field_errors() }}
|
||||
<div class="form-group row gap-2x">
|
||||
<div>{{ form.amount.as_field_group() }}</div>
|
||||
<div>
|
||||
{{ form.label.errors }}
|
||||
<label for="{{ form.label.id_for_label }}">{{ form.label.label }}</label>
|
||||
{{ form.label }}
|
||||
<span class="helptext">{{ form.label.help_text }}</span>
|
||||
</div>
|
||||
<div class="grow">{{ form.groups.as_field_group() }}</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div>
|
||||
{{ form.is_always_shown.errors }}
|
||||
<div class="row gap">
|
||||
{{ form.is_always_shown }}
|
||||
<label for="{{ form.is_always_shown.id_for_label }}">{{ form.is_always_shown.label }}</label>
|
||||
</div>
|
||||
<span class="helptext">{{ form.is_always_shown.help_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{%- if form.DELETE -%}
|
||||
<div class="form-group row gap">
|
||||
{{ form.DELETE.as_field_group() }}
|
||||
</div>
|
||||
{%- else -%}
|
||||
<br>
|
||||
<button
|
||||
class="btn btn-grey"
|
||||
@click.prevent="removeForm($event.target.closest('fieldset').parentElement)"
|
||||
>
|
||||
<i class="fa fa-minus"></i> {% trans %}Remove price{% endtrans %}
|
||||
</button>
|
||||
{%- endif -%}
|
||||
{%- for field in form.hidden_fields() -%}
|
||||
{{ field }}
|
||||
{%- endfor -%}
|
||||
</fieldset>
|
||||
<hr class="margin-bottom">
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
{% if object %}
|
||||
<h2>{% trans name=object %}Edit product {{ name }}{% endtrans %}</h2>
|
||||
@@ -49,7 +92,54 @@
|
||||
{% endif %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p() }}
|
||||
{{ form.non_field_errors() }}
|
||||
<fieldset class="row gap">
|
||||
<div>{{ form.name.as_field_group() }}</div>
|
||||
<div>{{ form.code.as_field_group() }}</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<div class="form-group">{{ form.description.as_field_group() }}</div>
|
||||
</fieldset>
|
||||
<fieldset class="row gap">
|
||||
<div>{{ form.club.as_field_group() }}</div>
|
||||
<div>{{ form.product_type.as_field_group() }}</div>
|
||||
</fieldset>
|
||||
<fieldset><div>{{ form.icon.as_field_group() }}</div></fieldset>
|
||||
<fieldset><div>{{ form.purchase_price.as_field_group() }}</div></fieldset>
|
||||
<fieldset>
|
||||
<div>{{ form.limit_age.as_field_group() }}</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<div class="row gap">
|
||||
{{ form.tray }}
|
||||
<div>
|
||||
{{ form.tray.label_tag() }}
|
||||
<span class="helptext">{{ form.tray.help_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset><div>{{ form.counters.as_field_group() }}</div></fieldset>
|
||||
|
||||
<h3 class="margin-bottom">{% trans %}Prices{% endtrans %}</h3>
|
||||
|
||||
<div x-data="dynamicFormSet({ prefix: '{{ form.price_formset.prefix }}' })">
|
||||
{{ form.price_formset.management_form }}
|
||||
<div x-ref="formContainer">
|
||||
{%- for form in form.price_formset.forms -%}
|
||||
<div>
|
||||
{{ price_form(form) }}
|
||||
</div>
|
||||
{%- endfor -%}
|
||||
</div>
|
||||
<template x-ref="formTemplate">
|
||||
<div>
|
||||
{{ price_form(form.price_formset.empty_form) }}
|
||||
</div>
|
||||
</template>
|
||||
<button class="btn btn-grey" @click.prevent="addForm()">
|
||||
<i class="fa fa-plus"></i> {% trans %}Add a price{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
@@ -64,7 +154,7 @@
|
||||
</em>
|
||||
</p>
|
||||
|
||||
<div x-data="dynamicFormSet" class="margin-bottom">
|
||||
<div x-data="dynamicFormSet({ prefix: '{{ form.action_formset.prefix }}' })" class="margin-bottom">
|
||||
{{ form.action_formset.management_form }}
|
||||
<div x-ref="formContainer">
|
||||
{%- for f in form.action_formset.forms -%}
|
||||
@@ -78,6 +168,7 @@
|
||||
<i class="fa fa-plus"></i>{% trans %}Add action{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="row gap margin-bottom">{{ form.archived.as_field_group() }}</div>
|
||||
<p><input class="btn btn-blue" type="submit" value="{% trans %}Save{% endtrans %}" /></p>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -108,7 +108,7 @@
|
||||
</template>
|
||||
<span class="card-content">
|
||||
<strong class="card-title" x-text="`${p.name} (${p.code})`"></strong>
|
||||
<p x-text="`${p.selling_price} €`"></p>
|
||||
<p x-text="`${p.prices.map((p) => p.amount).join(' – ')} €`"></p>
|
||||
</span>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@@ -16,7 +16,7 @@ from counter.forms import (
|
||||
ScheduledProductActionForm,
|
||||
ScheduledProductActionFormSet,
|
||||
)
|
||||
from counter.models import Product, ScheduledProductAction
|
||||
from counter.models import Product, ProductType, ScheduledProductAction
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -47,18 +47,19 @@ def test_create_actions_alongside_product():
|
||||
form = ProductForm(
|
||||
data={
|
||||
"name": "foo",
|
||||
"description": "bar",
|
||||
"product_type": product.product_type_id,
|
||||
"product_type": ProductType.objects.first(),
|
||||
"club": product.club_id,
|
||||
"code": "FOO",
|
||||
"purchase_price": 1.0,
|
||||
"selling_price": 1.0,
|
||||
"special_selling_price": 1.0,
|
||||
"limit_age": 0,
|
||||
"form-TOTAL_FORMS": "2",
|
||||
"form-INITIAL_FORMS": "0",
|
||||
"form-0-task": "counter.tasks.archive_product",
|
||||
"form-0-trigger_at": trigger_at,
|
||||
"price-TOTAL_FORMS": "0",
|
||||
"price-INITIAL_FORMS": "0",
|
||||
"action-TOTAL_FORMS": "1",
|
||||
"action-INITIAL_FORMS": "0",
|
||||
"action-0-task": "counter.tasks.archive_product",
|
||||
"action-0-trigger_at": trigger_at,
|
||||
},
|
||||
)
|
||||
assert form.is_valid()
|
||||
|
||||
@@ -20,7 +20,6 @@ import pytest
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Permission, make_password
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import resolve_url
|
||||
from django.test import Client, TestCase
|
||||
@@ -34,13 +33,13 @@ from pytest_django.asserts import assertRedirects
|
||||
|
||||
from club.models import Membership
|
||||
from core.baker_recipes import board_user, subscriber_user, very_old_subscriber_user
|
||||
from core.models import BanGroup, User
|
||||
from counter.baker_recipes import product_recipe, sale_recipe
|
||||
from core.models import BanGroup, Group, User
|
||||
from counter.baker_recipes import price_recipe, product_recipe, sale_recipe
|
||||
from counter.models import (
|
||||
Counter,
|
||||
Customer,
|
||||
Permanency,
|
||||
Product,
|
||||
ProductType,
|
||||
Refilling,
|
||||
ReturnableProduct,
|
||||
Selling,
|
||||
@@ -204,7 +203,7 @@ class TestRefilling(TestFullClickBase):
|
||||
|
||||
@dataclass
|
||||
class BasketItem:
|
||||
id: int | None = None
|
||||
price_id: int | None = None
|
||||
quantity: int | None = None
|
||||
|
||||
def to_form(self, index: int) -> dict[str, str]:
|
||||
@@ -236,38 +235,59 @@ class TestCounterClick(TestFullClickBase):
|
||||
cls.banned_counter_customer.ban_groups.add(
|
||||
BanGroup.objects.get(pk=settings.SITH_GROUP_BANNED_COUNTER_ID)
|
||||
)
|
||||
subscriber_group = Group.objects.get(id=settings.SITH_GROUP_SUBSCRIBERS_ID)
|
||||
old_subscriber_group = Group.objects.get(
|
||||
id=settings.SITH_GROUP_OLD_SUBSCRIBERS_ID
|
||||
)
|
||||
_product_recipe = product_recipe.extend(product_type=baker.make(ProductType))
|
||||
|
||||
cls.gift = product_recipe.make(
|
||||
selling_price="-1.5",
|
||||
special_selling_price="-1.5",
|
||||
cls.gift = price_recipe.make(
|
||||
amount=-1.5, groups=[subscriber_group], product=_product_recipe.make()
|
||||
)
|
||||
cls.beer = product_recipe.make(
|
||||
limit_age=18, selling_price=1.5, special_selling_price=1
|
||||
cls.beer = price_recipe.make(
|
||||
groups=[subscriber_group],
|
||||
amount=1.5,
|
||||
product=_product_recipe.make(limit_age=18),
|
||||
)
|
||||
cls.beer_tap = product_recipe.make(
|
||||
limit_age=18, tray=True, selling_price=1.5, special_selling_price=1
|
||||
cls.beer_tap = price_recipe.make(
|
||||
groups=[subscriber_group],
|
||||
amount=1.5,
|
||||
product=_product_recipe.make(limit_age=18, tray=True),
|
||||
)
|
||||
cls.snack = product_recipe.make(
|
||||
limit_age=0, selling_price=1.5, special_selling_price=1
|
||||
cls.snack = price_recipe.make(
|
||||
groups=[subscriber_group, old_subscriber_group],
|
||||
amount=1.5,
|
||||
product=_product_recipe.make(limit_age=0),
|
||||
)
|
||||
cls.stamps = product_recipe.make(
|
||||
limit_age=0, selling_price=1.5, special_selling_price=1
|
||||
cls.stamps = price_recipe.make(
|
||||
groups=[subscriber_group],
|
||||
amount=1.5,
|
||||
product=_product_recipe.make(limit_age=0),
|
||||
)
|
||||
ReturnableProduct.objects.all().delete()
|
||||
cls.cons = baker.make(Product, selling_price=1)
|
||||
cls.dcons = baker.make(Product, selling_price=-1)
|
||||
cls.cons = price_recipe.make(
|
||||
amount=1, groups=[subscriber_group], product=_product_recipe.make()
|
||||
)
|
||||
cls.dcons = price_recipe.make(
|
||||
amount=-1, groups=[subscriber_group], product=_product_recipe.make()
|
||||
)
|
||||
baker.make(
|
||||
ReturnableProduct,
|
||||
product=cls.cons,
|
||||
returned_product=cls.dcons,
|
||||
product=cls.cons.product,
|
||||
returned_product=cls.dcons.product,
|
||||
max_return=3,
|
||||
)
|
||||
|
||||
cls.counter.products.add(
|
||||
cls.gift, cls.beer, cls.beer_tap, cls.snack, cls.cons, cls.dcons
|
||||
cls.gift.product,
|
||||
cls.beer.product,
|
||||
cls.beer_tap.product,
|
||||
cls.snack.product,
|
||||
cls.cons.product,
|
||||
cls.dcons.product,
|
||||
)
|
||||
cls.other_counter.products.add(cls.snack)
|
||||
cls.club_counter.products.add(cls.stamps)
|
||||
cls.other_counter.products.add(cls.snack.product)
|
||||
cls.club_counter.products.add(cls.stamps.product)
|
||||
|
||||
def login_in_bar(self, barmen: User | None = None):
|
||||
used_barman = barmen if barmen is not None else self.barmen
|
||||
@@ -285,10 +305,7 @@ class TestCounterClick(TestFullClickBase):
|
||||
) -> HttpResponse:
|
||||
used_counter = counter if counter is not None else self.counter
|
||||
used_client = client if client is not None else self.client
|
||||
data = {
|
||||
"form-TOTAL_FORMS": str(len(basket)),
|
||||
"form-INITIAL_FORMS": "0",
|
||||
}
|
||||
data = {"form-TOTAL_FORMS": str(len(basket)), "form-INITIAL_FORMS": "0"}
|
||||
for index, item in enumerate(basket):
|
||||
data.update(item.to_form(index))
|
||||
return used_client.post(
|
||||
@@ -331,32 +348,22 @@ class TestCounterClick(TestFullClickBase):
|
||||
res = self.submit_basket(
|
||||
self.customer, [BasketItem(self.beer.id, 2), BasketItem(self.snack.id, 1)]
|
||||
)
|
||||
assert res.status_code == 302
|
||||
self.assertRedirects(res, self.counter.get_absolute_url())
|
||||
|
||||
assert self.updated_amount(self.customer) == Decimal("5.5")
|
||||
|
||||
# Test barmen special price
|
||||
|
||||
force_refill_user(self.barmen, 10)
|
||||
|
||||
assert (
|
||||
self.submit_basket(self.barmen, [BasketItem(self.beer.id, 1)])
|
||||
).status_code == 302
|
||||
|
||||
assert self.updated_amount(self.barmen) == Decimal(9)
|
||||
|
||||
def test_click_tray_price(self):
|
||||
force_refill_user(self.customer, 20)
|
||||
self.login_in_bar(self.barmen)
|
||||
|
||||
# Not applying tray price
|
||||
res = self.submit_basket(self.customer, [BasketItem(self.beer_tap.id, 2)])
|
||||
assert res.status_code == 302
|
||||
self.assertRedirects(res, self.counter.get_absolute_url())
|
||||
assert self.updated_amount(self.customer) == Decimal(17)
|
||||
|
||||
# Applying tray price
|
||||
res = self.submit_basket(self.customer, [BasketItem(self.beer_tap.id, 7)])
|
||||
assert res.status_code == 302
|
||||
self.assertRedirects(res, self.counter.get_absolute_url())
|
||||
assert self.updated_amount(self.customer) == Decimal(8)
|
||||
|
||||
def test_click_alcool_unauthorized(self):
|
||||
@@ -477,7 +484,8 @@ class TestCounterClick(TestFullClickBase):
|
||||
BasketItem(None, 1),
|
||||
BasketItem(self.beer.id, None),
|
||||
]:
|
||||
assert self.submit_basket(self.customer, [item]).status_code == 200
|
||||
res = self.submit_basket(self.customer, [item])
|
||||
assert res.status_code == 200
|
||||
assert self.updated_amount(self.customer) == Decimal(10)
|
||||
|
||||
def test_click_not_enough_money(self):
|
||||
@@ -506,29 +514,30 @@ class TestCounterClick(TestFullClickBase):
|
||||
res = self.submit_basket(
|
||||
self.customer, [BasketItem(self.beer.id, 1), BasketItem(self.gift.id, 1)]
|
||||
)
|
||||
assert res.status_code == 302
|
||||
self.assertRedirects(res, self.counter.get_absolute_url())
|
||||
|
||||
assert self.updated_amount(self.customer) == 0
|
||||
|
||||
def test_recordings(self):
|
||||
force_refill_user(self.customer, self.cons.selling_price * 3)
|
||||
force_refill_user(self.customer, self.cons.amount * 3)
|
||||
self.login_in_bar(self.barmen)
|
||||
res = self.submit_basket(self.customer, [BasketItem(self.cons.id, 3)])
|
||||
assert res.status_code == 302
|
||||
assert self.updated_amount(self.customer) == 0
|
||||
assert list(
|
||||
self.customer.customer.return_balances.values("returnable", "balance")
|
||||
) == [{"returnable": self.cons.cons.id, "balance": 3}]
|
||||
) == [{"returnable": self.cons.product.cons.id, "balance": 3}]
|
||||
|
||||
res = self.submit_basket(self.customer, [BasketItem(self.dcons.id, 3)])
|
||||
assert res.status_code == 302
|
||||
assert self.updated_amount(self.customer) == self.dcons.selling_price * -3
|
||||
assert self.updated_amount(self.customer) == self.dcons.amount * -3
|
||||
|
||||
res = self.submit_basket(
|
||||
self.customer, [BasketItem(self.dcons.id, self.dcons.dcons.max_return)]
|
||||
self.customer,
|
||||
[BasketItem(self.dcons.id, self.dcons.product.dcons.max_return)],
|
||||
)
|
||||
# from now on, the user amount should not change
|
||||
expected_amount = self.dcons.selling_price * (-3 - self.dcons.dcons.max_return)
|
||||
expected_amount = self.dcons.amount * (-3 - self.dcons.product.dcons.max_return)
|
||||
assert res.status_code == 302
|
||||
assert self.updated_amount(self.customer) == expected_amount
|
||||
|
||||
@@ -545,48 +554,57 @@ class TestCounterClick(TestFullClickBase):
|
||||
def test_recordings_when_negative(self):
|
||||
sale_recipe.make(
|
||||
customer=self.customer.customer,
|
||||
product=self.dcons,
|
||||
unit_price=self.dcons.selling_price,
|
||||
product=self.dcons.product,
|
||||
unit_price=self.dcons.amount,
|
||||
quantity=10,
|
||||
)
|
||||
self.customer.customer.update_returnable_balance()
|
||||
self.login_in_bar(self.barmen)
|
||||
res = self.submit_basket(self.customer, [BasketItem(self.dcons.id, 1)])
|
||||
assert res.status_code == 200
|
||||
assert self.updated_amount(self.customer) == self.dcons.selling_price * -10
|
||||
assert self.updated_amount(self.customer) == self.dcons.amount * -10
|
||||
|
||||
res = self.submit_basket(self.customer, [BasketItem(self.cons.id, 3)])
|
||||
assert res.status_code == 302
|
||||
assert (
|
||||
self.updated_amount(self.customer)
|
||||
== self.dcons.selling_price * -10 - self.cons.selling_price * 3
|
||||
== self.dcons.amount * -10 - self.cons.amount * 3
|
||||
)
|
||||
|
||||
res = self.submit_basket(self.customer, [BasketItem(self.beer.id, 1)])
|
||||
assert res.status_code == 302
|
||||
assert (
|
||||
self.updated_amount(self.customer)
|
||||
== self.dcons.selling_price * -10
|
||||
- self.cons.selling_price * 3
|
||||
- self.beer.selling_price
|
||||
== self.dcons.amount * -10 - self.cons.amount * 3 - self.beer.amount
|
||||
)
|
||||
|
||||
def test_no_fetch_archived_product(self):
|
||||
counter = baker.make(Counter)
|
||||
group = baker.make(Group)
|
||||
customer = baker.make(Customer)
|
||||
product_recipe.make(archived=True, counters=[counter])
|
||||
unarchived_products = product_recipe.make(
|
||||
archived=False, counters=[counter], _quantity=3
|
||||
group.users.add(customer.user)
|
||||
_product_recipe = product_recipe.extend(
|
||||
counters=[counter], product_type=baker.make(ProductType)
|
||||
)
|
||||
customer_products = counter.get_products_for(customer)
|
||||
assert unarchived_products == customer_products
|
||||
price_recipe.make(
|
||||
_quantity=2,
|
||||
product=iter(_product_recipe.make(archived=True, _quantity=2)),
|
||||
groups=[group],
|
||||
)
|
||||
unarchived_prices = price_recipe.make(
|
||||
_quantity=2,
|
||||
product=iter(_product_recipe.make(archived=False, _quantity=2)),
|
||||
groups=[group],
|
||||
)
|
||||
customer_prices = counter.get_prices_for(customer)
|
||||
assert unarchived_prices == customer_prices
|
||||
|
||||
|
||||
class TestCounterStats(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.users = subscriber_user.make(_quantity=4)
|
||||
product = product_recipe.make(selling_price=1)
|
||||
product = price_recipe.make(amount=1).product
|
||||
cls.counter = baker.make(
|
||||
Counter, type=["BAR"], sellers=cls.users[:4], products=[product]
|
||||
)
|
||||
@@ -785,9 +803,6 @@ class TestClubCounterClickAccess(TestCase):
|
||||
|
||||
cls.user = subscriber_user.make()
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
|
||||
def test_anonymous(self):
|
||||
res = self.client.get(self.click_url)
|
||||
assert res.status_code == 403
|
||||
|
||||
@@ -341,7 +341,7 @@ def test_update_balance():
|
||||
def test_update_returnable_balance():
|
||||
ReturnableProduct.objects.all().delete()
|
||||
customer = baker.make(Customer)
|
||||
products = product_recipe.make(selling_price=0, _quantity=4, _bulk_create=True)
|
||||
products = product_recipe.make(_quantity=4, _bulk_create=True)
|
||||
returnables = [
|
||||
baker.make(
|
||||
ReturnableProduct, product=products[0], returned_product=products[1]
|
||||
|
||||
@@ -7,12 +7,7 @@ from counter.forms import ProductFormulaForm
|
||||
class TestFormulaForm(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.products = product_recipe.make(
|
||||
selling_price=iter([1.5, 1, 1]),
|
||||
special_selling_price=iter([1.4, 0.9, 0.9]),
|
||||
_quantity=3,
|
||||
_bulk_create=True,
|
||||
)
|
||||
cls.products = product_recipe.make(_quantity=3, _bulk_create=True)
|
||||
|
||||
def test_ok(self):
|
||||
form = ProductFormulaForm(
|
||||
@@ -26,23 +21,6 @@ class TestFormulaForm(TestCase):
|
||||
assert formula.result == self.products[0]
|
||||
assert set(formula.products.all()) == set(self.products[1:])
|
||||
|
||||
def test_price_invalid(self):
|
||||
self.products[0].selling_price = 2.1
|
||||
self.products[0].save()
|
||||
form = ProductFormulaForm(
|
||||
data={
|
||||
"result": self.products[0].id,
|
||||
"products": [self.products[1].id, self.products[2].id],
|
||||
}
|
||||
)
|
||||
assert not form.is_valid()
|
||||
assert form.errors == {
|
||||
"result": [
|
||||
"Le résultat ne peut pas être plus cher "
|
||||
"que le total des autres produits."
|
||||
]
|
||||
}
|
||||
|
||||
def test_product_both_in_result_and_products(self):
|
||||
form = ProductFormulaForm(
|
||||
data={
|
||||
|
||||
+100
-37
@@ -9,6 +9,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
from model_bakery import baker
|
||||
from model_bakery.recipe import Recipe
|
||||
from PIL import Image
|
||||
from pytest_django.asserts import assertNumQueries, assertRedirects
|
||||
|
||||
@@ -16,8 +17,8 @@ from club.models import Club
|
||||
from core.baker_recipes import board_user, subscriber_user
|
||||
from core.models import Group, User
|
||||
from counter.baker_recipes import product_recipe
|
||||
from counter.forms import ProductForm
|
||||
from counter.models import Product, ProductFormula, ProductType
|
||||
from counter.forms import ProductForm, ProductPriceFormSet
|
||||
from counter.models import Price, Product, ProductType
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -81,11 +82,11 @@ def test_fetch_product_access(
|
||||
def test_fetch_product_nb_queries(client: Client):
|
||||
client.force_login(baker.make(User, is_superuser=True))
|
||||
cache.clear()
|
||||
with assertNumQueries(5):
|
||||
with assertNumQueries(6):
|
||||
# - 2 for authentication
|
||||
# - 1 for pagination
|
||||
# - 1 for the actual request
|
||||
# - 1 to prefetch the related buying_groups
|
||||
# - 2 to prefetch the related prices and groups
|
||||
client.get(reverse("api:search_products_detailed"))
|
||||
|
||||
|
||||
@@ -107,48 +108,21 @@ class TestCreateProduct(TestCase):
|
||||
"selling_price": 1.0,
|
||||
"special_selling_price": 1.0,
|
||||
"limit_age": 0,
|
||||
"form-TOTAL_FORMS": 0,
|
||||
"form-INITIAL_FORMS": 0,
|
||||
"price-TOTAL_FORMS": 0,
|
||||
"price-INITIAL_FORMS": 0,
|
||||
"action-TOTAL_FORMS": 0,
|
||||
"action-INITIAL_FORMS": 0,
|
||||
}
|
||||
|
||||
def test_form(self):
|
||||
def test_form_simple(self):
|
||||
form = ProductForm(data=self.data)
|
||||
assert form.is_valid()
|
||||
instance = form.save()
|
||||
assert instance.club == self.club
|
||||
assert instance.product_type == self.product_type
|
||||
assert instance.name == "foo"
|
||||
assert instance.selling_price == 1.0
|
||||
|
||||
def test_form_with_product_from_formula(self):
|
||||
"""Test when the edited product is a result of a formula."""
|
||||
self.client.force_login(self.counter_admin)
|
||||
products = product_recipe.make(
|
||||
selling_price=iter([1.5, 1, 1]),
|
||||
special_selling_price=iter([1.4, 0.9, 0.9]),
|
||||
_quantity=3,
|
||||
_bulk_create=True,
|
||||
)
|
||||
baker.make(ProductFormula, result=products[0], products=products[1:])
|
||||
|
||||
data = self.data | {"selling_price": 1.7, "special_selling_price": 1.5}
|
||||
form = ProductForm(data=data, instance=products[0])
|
||||
assert form.is_valid()
|
||||
|
||||
# it shouldn't be possible to give a price higher than the formula's products
|
||||
data = self.data | {"selling_price": 2.1, "special_selling_price": 1.9}
|
||||
form = ProductForm(data=data, instance=products[0])
|
||||
assert not form.is_valid()
|
||||
assert form.errors == {
|
||||
"selling_price": [
|
||||
"Assurez-vous que cette valeur est inférieure ou égale à 2.00."
|
||||
],
|
||||
"special_selling_price": [
|
||||
"Assurez-vous que cette valeur est inférieure ou égale à 1.80."
|
||||
],
|
||||
}
|
||||
|
||||
def test_view(self):
|
||||
def test_view_simple(self):
|
||||
self.client.force_login(self.counter_admin)
|
||||
url = reverse("counter:new_product")
|
||||
response = self.client.get(url)
|
||||
@@ -159,3 +133,92 @@ class TestCreateProduct(TestCase):
|
||||
assert product.name == "foo"
|
||||
assert product.club == self.club
|
||||
assert product.product_type == self.product_type
|
||||
|
||||
|
||||
class TestPriceFormSet(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
cls.product = product_recipe.make()
|
||||
cls.counter_admin = baker.make(
|
||||
User, groups=[Group.objects.get(id=settings.SITH_GROUP_COUNTER_ADMIN_ID)]
|
||||
)
|
||||
cls.groups = baker.make(Group, _quantity=3)
|
||||
|
||||
def test_add_price(self):
|
||||
data = {
|
||||
"prices-0-amount": 2,
|
||||
"prices-0-label": "foo",
|
||||
"prices-0-groups": [self.groups[0].id, self.groups[1].id],
|
||||
"prices-0-is_always_shown": True,
|
||||
"prices-1-amount": 1.5,
|
||||
"prices-1-label": "",
|
||||
"prices-1-groups": [self.groups[1].id, self.groups[2].id],
|
||||
"prices-1-is_always_shown": False,
|
||||
"prices-TOTAL_FORMS": 2,
|
||||
"prices-INITIAL_FORMS": 0,
|
||||
}
|
||||
form = ProductPriceFormSet(instance=self.product, data=data)
|
||||
assert form.is_valid()
|
||||
form.save()
|
||||
prices = list(self.product.prices.order_by("amount"))
|
||||
assert len(prices) == 2
|
||||
assert prices[0].amount == 1.5
|
||||
assert prices[0].label == ""
|
||||
assert prices[0].is_always_shown is False
|
||||
assert set(prices[0].groups.all()) == {self.groups[1], self.groups[2]}
|
||||
assert prices[1].amount == 2
|
||||
assert prices[1].label == "foo"
|
||||
assert prices[1].is_always_shown is True
|
||||
assert set(prices[1].groups.all()) == {self.groups[0], self.groups[1]}
|
||||
|
||||
def test_change_prices(self):
|
||||
price_a = baker.make(
|
||||
Price, product=self.product, amount=1.5, groups=self.groups[:1]
|
||||
)
|
||||
price_b = baker.make(
|
||||
Price, product=self.product, amount=2, groups=self.groups[1:]
|
||||
)
|
||||
data = {
|
||||
"prices-0-id": price_a.id,
|
||||
"prices-0-DELETE": True,
|
||||
"prices-1-id": price_b.id,
|
||||
"prices-1-DELETE": False,
|
||||
"prices-1-amount": 3,
|
||||
"prices-1-label": "foo",
|
||||
"prices-1-groups": [self.groups[1].id],
|
||||
"prices-1-is_always_shown": True,
|
||||
"prices-TOTAL_FORMS": 2,
|
||||
"prices-INITIAL_FORMS": 2,
|
||||
}
|
||||
form = ProductPriceFormSet(instance=self.product, data=data)
|
||||
assert form.is_valid()
|
||||
form.save()
|
||||
prices = list(self.product.prices.order_by("amount"))
|
||||
assert len(prices) == 1
|
||||
assert prices[0].amount == 3
|
||||
assert prices[0].label == "foo"
|
||||
assert prices[0].is_always_shown is True
|
||||
assert set(prices[0].groups.all()) == {self.groups[1]}
|
||||
assert not Price.objects.filter(id=price_a.id).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_price_for_user():
|
||||
groups = baker.make(Group, _quantity=4)
|
||||
users = [
|
||||
baker.make(User, groups=groups[:2]),
|
||||
baker.make(User, groups=groups[1:3]),
|
||||
baker.make(User, groups=[groups[3]]),
|
||||
]
|
||||
recipe = Recipe(Price, product=product_recipe.make())
|
||||
prices = [
|
||||
recipe.make(amount=5, groups=groups, is_always_shown=True),
|
||||
recipe.make(amount=4, groups=[groups[0]], is_always_shown=True),
|
||||
recipe.make(amount=3, groups=[groups[1]], is_always_shown=False),
|
||||
recipe.make(amount=2, groups=[groups[3]], is_always_shown=False),
|
||||
recipe.make(amount=1, groups=[groups[1]], is_always_shown=False),
|
||||
]
|
||||
qs = Price.objects.order_by("-amount")
|
||||
assert set(qs.for_user(users[0])) == {prices[0], prices[1], prices[4]}
|
||||
assert set(qs.for_user(users[1])) == {prices[0], prices[4]}
|
||||
assert set(qs.for_user(users[2])) == {prices[0], prices[3]}
|
||||
|
||||
+20
-22
@@ -73,7 +73,7 @@ class CounterClick(
|
||||
kwargs["form_kwargs"] = {
|
||||
"customer": self.customer,
|
||||
"counter": self.object,
|
||||
"allowed_products": {product.id: product for product in self.products},
|
||||
"allowed_prices": {price.id: price for price in self.prices},
|
||||
}
|
||||
return kwargs
|
||||
|
||||
@@ -103,7 +103,7 @@ class CounterClick(
|
||||
):
|
||||
return redirect(obj) # Redirect to counter
|
||||
|
||||
self.products = obj.get_products_for(self.customer)
|
||||
self.prices = obj.get_prices_for(self.customer)
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -121,32 +121,31 @@ class CounterClick(
|
||||
# This is important because some items have a negative price
|
||||
# Negative priced items gives money to the customer and should
|
||||
# be processed first so that we don't throw a not enough money error
|
||||
for form in sorted(formset, key=lambda form: form.product.price):
|
||||
for form in sorted(formset, key=lambda form: form.price.amount):
|
||||
self.request.session["last_basket"].append(
|
||||
f"{form.cleaned_data['quantity']} x {form.product.name}"
|
||||
f"{form.cleaned_data['quantity']} x {form.price.full_label}"
|
||||
)
|
||||
|
||||
common_kwargs = {
|
||||
"product": form.price.product,
|
||||
"club_id": form.price.product.club_id,
|
||||
"counter": self.object,
|
||||
"seller": operator,
|
||||
"customer": self.customer,
|
||||
}
|
||||
Selling(
|
||||
label=form.product.name,
|
||||
product=form.product,
|
||||
club=form.product.club,
|
||||
counter=self.object,
|
||||
unit_price=form.product.price,
|
||||
**common_kwargs,
|
||||
label=form.price.full_label,
|
||||
unit_price=form.price.amount,
|
||||
quantity=form.cleaned_data["quantity"]
|
||||
- form.cleaned_data["bonus_quantity"],
|
||||
seller=operator,
|
||||
customer=self.customer,
|
||||
).save()
|
||||
if form.cleaned_data["bonus_quantity"] > 0:
|
||||
Selling(
|
||||
label=f"{form.product.name} (Plateau)",
|
||||
product=form.product,
|
||||
club=form.product.club,
|
||||
counter=self.object,
|
||||
**common_kwargs,
|
||||
label=f"{form.price.full_label} (Plateau)",
|
||||
unit_price=0,
|
||||
quantity=form.cleaned_data["bonus_quantity"],
|
||||
seller=operator,
|
||||
customer=self.customer,
|
||||
).save()
|
||||
|
||||
self.customer.update_returnable_balance()
|
||||
@@ -207,14 +206,13 @@ class CounterClick(
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add customer to the context."""
|
||||
kwargs = super().get_context_data(**kwargs)
|
||||
kwargs["products"] = self.products
|
||||
kwargs["prices"] = self.prices
|
||||
kwargs["formulas"] = ProductFormula.objects.filter(
|
||||
result__in=self.products
|
||||
result__in=[p.product_id for p in self.prices]
|
||||
).prefetch_related("products")
|
||||
kwargs["categories"] = defaultdict(list)
|
||||
for product in kwargs["products"]:
|
||||
if product.product_type:
|
||||
kwargs["categories"][product.product_type].append(product)
|
||||
for price in self.prices:
|
||||
kwargs["categories"][price.product.product_type].append(price)
|
||||
kwargs["customer"] = self.customer
|
||||
kwargs["cancel_url"] = self.get_success_url()
|
||||
|
||||
|
||||
+7
-7
@@ -22,23 +22,22 @@ from eboutic.models import Basket, BasketItem, Invoice, InvoiceItem
|
||||
class BasketAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "date", "total")
|
||||
autocomplete_fields = ("user",)
|
||||
date_hierarchy = "date"
|
||||
|
||||
def get_queryset(self, request):
|
||||
return (
|
||||
super()
|
||||
.get_queryset(request)
|
||||
.annotate(
|
||||
total=Sum(
|
||||
F("items__quantity") * F("items__product_unit_price"), default=0
|
||||
)
|
||||
total=Sum(F("items__quantity") * F("items__unit_price"), default=0)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@admin.register(BasketItem)
|
||||
class BasketItemAdmin(admin.ModelAdmin):
|
||||
list_display = ("basket", "product_name", "product_unit_price", "quantity")
|
||||
search_fields = ("product_name",)
|
||||
list_display = ("label", "unit_price", "quantity")
|
||||
search_fields = ("label",)
|
||||
|
||||
|
||||
@admin.register(Invoice)
|
||||
@@ -50,5 +49,6 @@ class InvoiceAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(InvoiceItem)
|
||||
class InvoiceItemAdmin(admin.ModelAdmin):
|
||||
list_display = ("invoice", "product_name", "product_unit_price", "quantity")
|
||||
search_fields = ("product_name",)
|
||||
list_display = ("label", "unit_price", "quantity")
|
||||
search_fields = ("label",)
|
||||
list_select_related = ("price",)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Generated by Django 5.2.11 on 2026-02-22 18:13
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("counter", "0039_price"), ("eboutic", "0002_auto_20221005_2243")]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="basketitem", old_name="product_name", new_name="label"
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="basketitem",
|
||||
old_name="product_unit_price",
|
||||
new_name="unit_price",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="basketitem", old_name="product_id", new_name="product"
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="invoiceitem", old_name="product_name", new_name="label"
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="invoiceitem",
|
||||
old_name="product_unit_price",
|
||||
new_name="unit_price",
|
||||
),
|
||||
migrations.RenameField(
|
||||
model_name="invoiceitem", old_name="product_id", new_name="product"
|
||||
),
|
||||
migrations.RemoveField(model_name="basketitem", name="type_id"),
|
||||
migrations.RemoveField(model_name="invoiceitem", name="type_id"),
|
||||
migrations.AlterField(
|
||||
model_name="basketitem",
|
||||
name="product",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="counter.product",
|
||||
verbose_name="product",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="invoiceitem",
|
||||
name="product",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="counter.product",
|
||||
verbose_name="product",
|
||||
),
|
||||
),
|
||||
]
|
||||
+49
-90
@@ -17,7 +17,7 @@ from __future__ import annotations
|
||||
import hmac
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Self
|
||||
from typing import Self
|
||||
|
||||
from dict2xml import dict2xml
|
||||
from django.conf import settings
|
||||
@@ -30,8 +30,8 @@ from core.models import User
|
||||
from counter.fields import CurrencyField
|
||||
from counter.models import (
|
||||
BillingInfo,
|
||||
Counter,
|
||||
Customer,
|
||||
Price,
|
||||
Product,
|
||||
Refilling,
|
||||
Selling,
|
||||
@@ -39,22 +39,6 @@ from counter.models import (
|
||||
)
|
||||
|
||||
|
||||
def get_eboutic_products(user: User) -> list[Product]:
|
||||
products = (
|
||||
get_eboutic()
|
||||
.products.filter(product_type__isnull=False)
|
||||
.filter(archived=False, limit_age__lte=user.age)
|
||||
.annotate(
|
||||
order=F("product_type__order"),
|
||||
category=F("product_type__name"),
|
||||
category_comment=F("product_type__comment"),
|
||||
price=F("selling_price"), # <-- selected price for basket validation
|
||||
)
|
||||
.prefetch_related("buying_groups") # <-- used in `Product.can_be_sold_to`
|
||||
)
|
||||
return [p for p in products if p.can_be_sold_to(user)]
|
||||
|
||||
|
||||
class BillingInfoState(Enum):
|
||||
VALID = 1
|
||||
EMPTY = 2
|
||||
@@ -94,21 +78,21 @@ class Basket(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.user}'s basket ({self.items.all().count()} items)"
|
||||
|
||||
def can_be_viewed_by(self, user):
|
||||
def can_be_viewed_by(self, user: User):
|
||||
return self.user == user
|
||||
|
||||
@cached_property
|
||||
def contains_refilling_item(self) -> bool:
|
||||
return self.items.filter(
|
||||
type_id=settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
||||
product__product_type_id=settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
||||
).exists()
|
||||
|
||||
@cached_property
|
||||
def total(self) -> float:
|
||||
return float(
|
||||
self.items.aggregate(
|
||||
total=Sum(F("quantity") * F("product_unit_price"), default=0)
|
||||
)["total"]
|
||||
self.items.aggregate(total=Sum(F("quantity") * F("unit_price"), default=0))[
|
||||
"total"
|
||||
]
|
||||
)
|
||||
|
||||
def generate_sales(
|
||||
@@ -120,7 +104,8 @@ class Basket(models.Model):
|
||||
Example:
|
||||
```python
|
||||
counter = Counter.objects.get(name="Eboutic")
|
||||
sales = basket.generate_sales(counter, "SITH_ACCOUNT")
|
||||
user = User.objects.get(username="bibou")
|
||||
sales = basket.generate_sales(counter, user, Selling.PaymentMethod.SITH_ACCOUNT)
|
||||
# here the basket is in the same state as before the method call
|
||||
|
||||
with transaction.atomic():
|
||||
@@ -131,31 +116,23 @@ class Basket(models.Model):
|
||||
# thus only the sales remain
|
||||
```
|
||||
"""
|
||||
# I must proceed with two distinct requests instead of
|
||||
# only one with a join because the AbstractBaseItem model has been
|
||||
# poorly designed. If you refactor the model, please refactor this too.
|
||||
items = self.items.order_by("product_id")
|
||||
ids = [item.product_id for item in items]
|
||||
products = Product.objects.filter(id__in=ids).order_by("id")
|
||||
# items and products are sorted in the same order
|
||||
sales = []
|
||||
for item, product in zip(items, products, strict=False):
|
||||
sales.append(
|
||||
customer = Customer.get_or_create(self.user)[0]
|
||||
return [
|
||||
Selling(
|
||||
label=product.name,
|
||||
label=item.label,
|
||||
counter=counter,
|
||||
club=product.club,
|
||||
product=product,
|
||||
club_id=item.product.club_id,
|
||||
product=item.product,
|
||||
seller=seller,
|
||||
customer=Customer.get_or_create(self.user)[0],
|
||||
unit_price=item.product_unit_price,
|
||||
customer=customer,
|
||||
unit_price=item.unit_price,
|
||||
quantity=item.quantity,
|
||||
payment_method=payment_method,
|
||||
)
|
||||
)
|
||||
return sales
|
||||
for item in self.items.select_related("product")
|
||||
]
|
||||
|
||||
def get_e_transaction_data(self) -> list[tuple[str, Any]]:
|
||||
def get_e_transaction_data(self) -> list[tuple[str, str]]:
|
||||
user = self.user
|
||||
if not hasattr(user, "customer"):
|
||||
raise Customer.DoesNotExist
|
||||
@@ -201,7 +178,7 @@ class InvoiceQueryset(models.QuerySet):
|
||||
def annotate_total(self) -> Self:
|
||||
"""Annotate the queryset with the total amount of each invoice.
|
||||
|
||||
The total amount is the sum of (product_unit_price * quantity)
|
||||
The total amount is the sum of (unit_price * quantity)
|
||||
for all items related to the invoice.
|
||||
"""
|
||||
# aggregates within subqueries require a little bit of black magic,
|
||||
@@ -211,7 +188,7 @@ class InvoiceQueryset(models.QuerySet):
|
||||
total=Subquery(
|
||||
InvoiceItem.objects.filter(invoice_id=OuterRef("pk"))
|
||||
.values("invoice_id")
|
||||
.annotate(total=Sum(F("product_unit_price") * F("quantity")))
|
||||
.annotate(total=Sum(F("unit_price") * F("quantity")))
|
||||
.values("total")
|
||||
)
|
||||
)
|
||||
@@ -221,11 +198,7 @@ class Invoice(models.Model):
|
||||
"""Invoices are generated once the payment has been validated."""
|
||||
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
related_name="invoices",
|
||||
verbose_name=_("user"),
|
||||
blank=False,
|
||||
on_delete=models.CASCADE,
|
||||
User, related_name="invoices", verbose_name=_("user"), on_delete=models.CASCADE
|
||||
)
|
||||
date = models.DateTimeField(_("date"), auto_now=True)
|
||||
validated = models.BooleanField(_("validated"), default=False)
|
||||
@@ -246,53 +219,44 @@ class Invoice(models.Model):
|
||||
if self.validated:
|
||||
raise DataError(_("Invoice already validated"))
|
||||
customer, _created = Customer.get_or_create(user=self.user)
|
||||
eboutic = Counter.objects.filter(type="EBOUTIC").first()
|
||||
for i in self.items.all():
|
||||
if i.type_id == settings.SITH_COUNTER_PRODUCTTYPE_REFILLING:
|
||||
new = Refilling(
|
||||
counter=eboutic,
|
||||
customer=customer,
|
||||
operator=self.user,
|
||||
amount=i.product_unit_price * i.quantity,
|
||||
payment_method=Refilling.PaymentMethod.CARD,
|
||||
date=self.date,
|
||||
kwargs = {
|
||||
"counter": get_eboutic(),
|
||||
"customer": customer,
|
||||
"date": self.date,
|
||||
"payment_method": Selling.PaymentMethod.CARD,
|
||||
}
|
||||
for i in self.items.select_related("product"):
|
||||
if i.product.product_type_id == settings.SITH_COUNTER_PRODUCTTYPE_REFILLING:
|
||||
Refilling.objects.create(
|
||||
**kwargs, operator=self.user, amount=i.unit_price * i.quantity
|
||||
)
|
||||
new.save()
|
||||
else:
|
||||
product = Product.objects.filter(id=i.product_id).first()
|
||||
new = Selling(
|
||||
label=i.product_name,
|
||||
counter=eboutic,
|
||||
club=product.club,
|
||||
product=product,
|
||||
Selling.objects.create(
|
||||
**kwargs,
|
||||
label=i.label,
|
||||
club_id=i.product.club_id,
|
||||
product=i.product,
|
||||
seller=self.user,
|
||||
customer=customer,
|
||||
unit_price=i.product_unit_price,
|
||||
unit_price=i.unit_price,
|
||||
quantity=i.quantity,
|
||||
payment_method=Selling.PaymentMethod.CARD,
|
||||
date=self.date,
|
||||
)
|
||||
new.save()
|
||||
self.validated = True
|
||||
self.save()
|
||||
|
||||
|
||||
class AbstractBaseItem(models.Model):
|
||||
product_id = models.IntegerField(_("product id"))
|
||||
product_name = models.CharField(_("product name"), max_length=255)
|
||||
type_id = models.IntegerField(_("product type id"))
|
||||
product_unit_price = CurrencyField(_("unit price"))
|
||||
product = models.ForeignKey(
|
||||
Product, verbose_name=_("product"), on_delete=models.PROTECT
|
||||
)
|
||||
label = models.CharField(_("product name"), max_length=255)
|
||||
unit_price = CurrencyField(_("unit price"))
|
||||
quantity = models.PositiveIntegerField(_("quantity"))
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __str__(self):
|
||||
return "Item: %s (%s) x%d" % (
|
||||
self.product_name,
|
||||
self.product_unit_price,
|
||||
self.quantity,
|
||||
)
|
||||
return "Item: %s (%s) x%d" % (self.product.name, self.unit_price, self.quantity)
|
||||
|
||||
|
||||
class BasketItem(AbstractBaseItem):
|
||||
@@ -301,21 +265,16 @@ class BasketItem(AbstractBaseItem):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_product(cls, product: Product, quantity: int, basket: Basket):
|
||||
def from_price(cls, price: Price, quantity: int, basket: Basket):
|
||||
"""Create a BasketItem with the same characteristics as the
|
||||
product passed in parameters, with the specified quantity.
|
||||
|
||||
Warning:
|
||||
the basket field is not filled, so you must set
|
||||
it yourself before saving the model.
|
||||
product price passed in parameters, with the specified quantity.
|
||||
"""
|
||||
return cls(
|
||||
basket=basket,
|
||||
product_id=product.id,
|
||||
product_name=product.name,
|
||||
type_id=product.product_type_id,
|
||||
label=price.full_label,
|
||||
product_id=price.product_id,
|
||||
quantity=quantity,
|
||||
product_unit_price=product.selling_price,
|
||||
unit_price=price.amount,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
export {};
|
||||
|
||||
interface BasketItem {
|
||||
id: number;
|
||||
priceId: number;
|
||||
name: string;
|
||||
quantity: number;
|
||||
// biome-ignore lint/style/useNamingConvention: the python code is snake_case
|
||||
unit_price: number;
|
||||
unitPrice: number;
|
||||
}
|
||||
|
||||
// increment the key number if the data schema of the cached basket changes
|
||||
const BASKET_CACHE_KEY = "basket1";
|
||||
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("basket", (lastPurchaseTime?: number) => ({
|
||||
basket: [] as BasketItem[],
|
||||
@@ -30,24 +32,24 @@ document.addEventListener("alpine:init", () => {
|
||||
// It's quite tricky to manually apply attributes to the management part
|
||||
// of a formset so we dynamically apply it here
|
||||
this.$refs.basketManagementForm
|
||||
.querySelector("#id_form-TOTAL_FORMS")
|
||||
.getElementById("#id_form-TOTAL_FORMS")
|
||||
.setAttribute(":value", "basket.length");
|
||||
},
|
||||
|
||||
loadBasket(): BasketItem[] {
|
||||
if (localStorage.basket === undefined) {
|
||||
if (localStorage.getItem(BASKET_CACHE_KEY) === null) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
return JSON.parse(localStorage.basket);
|
||||
return JSON.parse(localStorage.getItem(BASKET_CACHE_KEY));
|
||||
} catch (_err) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
saveBasket() {
|
||||
localStorage.basket = JSON.stringify(this.basket);
|
||||
localStorage.basketTimestamp = Date.now();
|
||||
localStorage.setItem(BASKET_CACHE_KEY, JSON.stringify(this.basket));
|
||||
localStorage.setItem("basketTimestamp", Date.now().toString());
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -56,7 +58,7 @@ document.addEventListener("alpine:init", () => {
|
||||
*/
|
||||
getTotal() {
|
||||
return this.basket.reduce(
|
||||
(acc: number, item: BasketItem) => acc + item.quantity * item.unit_price,
|
||||
(acc: number, item: BasketItem) => acc + item.quantity * item.unitPrice,
|
||||
0,
|
||||
);
|
||||
},
|
||||
@@ -74,7 +76,7 @@ document.addEventListener("alpine:init", () => {
|
||||
* @param itemId the id of the item to remove
|
||||
*/
|
||||
remove(itemId: number) {
|
||||
const index = this.basket.findIndex((e: BasketItem) => e.id === itemId);
|
||||
const index = this.basket.findIndex((e: BasketItem) => e.priceId === itemId);
|
||||
|
||||
if (index < 0) {
|
||||
return;
|
||||
@@ -83,7 +85,7 @@ document.addEventListener("alpine:init", () => {
|
||||
|
||||
if (this.basket[index].quantity === 0) {
|
||||
this.basket = this.basket.filter(
|
||||
(e: BasketItem) => e.id !== this.basket[index].id,
|
||||
(e: BasketItem) => e.priceId !== this.basket[index].id,
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -104,11 +106,10 @@ document.addEventListener("alpine:init", () => {
|
||||
*/
|
||||
createItem(id: number, name: string, price: number): BasketItem {
|
||||
const newItem = {
|
||||
id,
|
||||
priceId: id,
|
||||
name,
|
||||
quantity: 0,
|
||||
// biome-ignore lint/style/useNamingConvention: the python code is snake_case
|
||||
unit_price: price,
|
||||
unitPrice: price,
|
||||
} as BasketItem;
|
||||
|
||||
this.basket.push(newItem);
|
||||
@@ -125,7 +126,7 @@ document.addEventListener("alpine:init", () => {
|
||||
* @param price The unit price of the product
|
||||
*/
|
||||
addFromCatalog(id: number, name: string, price: number) {
|
||||
let item = this.basket.find((e: BasketItem) => e.id === id);
|
||||
let item = this.basket.find((e: BasketItem) => e.priceId === id);
|
||||
|
||||
// if the item is not in the basket, we create it
|
||||
// else we add + 1 to it
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
<tbody>
|
||||
{% for item in basket.items.all() %}
|
||||
<tr>
|
||||
<td>{{ item.product_name }}</td>
|
||||
<td>{{ item.label }}</td>
|
||||
<td>{{ item.quantity }}</td>
|
||||
<td>{{ item.product_unit_price }} €</td>
|
||||
<td>{{ item.unit_price }} €</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -51,15 +51,15 @@
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<template x-for="(item, index) in Object.values(basket)" :key="item.id">
|
||||
<template x-for="(item, index) in Object.values(basket)" :key="item.priceId">
|
||||
<li class="item-row" x-show="item.quantity > 0">
|
||||
<div class="item-quantity">
|
||||
<i class="fa fa-minus fa-xs" @click="remove(item.id)"></i>
|
||||
<i class="fa fa-minus fa-xs" @click="remove(item.priceId)"></i>
|
||||
<span x-text="item.quantity"></span>
|
||||
<i class="fa fa-plus" @click="add(item)"></i>
|
||||
</div>
|
||||
<span class="item-name" x-text="item.name"></span>
|
||||
<span class="item-price" x-text="(item.unit_price * item.quantity).toFixed(2) + ' €'"></span>
|
||||
<span class="item-price" x-text="(item.unitPrice * item.quantity).toFixed(2) + ' €'"></span>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
@@ -71,9 +71,9 @@
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
:value="item.id"
|
||||
:id="`id_form-${index}-id`"
|
||||
:name="`form-${index}-id`"
|
||||
:value="item.priceId"
|
||||
:id="`id_form-${index}-price_id`"
|
||||
:name="`form-${index}-price_id`"
|
||||
required
|
||||
readonly
|
||||
>
|
||||
@@ -166,45 +166,40 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% for priority_groups in products|groupby('order') %}
|
||||
{% for category, items in priority_groups.list|groupby('category') %}
|
||||
{% if items|count > 0 %}
|
||||
{% for prices in categories %}
|
||||
{% set category = prices[0].product.product_type %}
|
||||
<section>
|
||||
{# I would have wholeheartedly directly used the header element instead
|
||||
but it has already been made messy in core/style.scss #}
|
||||
<div class="category-header">
|
||||
<h3>{{ category }}</h3>
|
||||
{% if items[0].category_comment %}
|
||||
<p><i>{{ items[0].category_comment }}</i></p>
|
||||
<h3>{{ category.name }}</h3>
|
||||
{% if category.comment %}
|
||||
<p><i>{{ category.comment }}</i></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="product-group">
|
||||
{% for p in items %}
|
||||
{% for price in prices %}
|
||||
<button
|
||||
id="{{ p.id }}"
|
||||
id="{{ price.id }}"
|
||||
class="card product-button clickable shadow"
|
||||
:class="{selected: basket.some((i) => i.id === {{ p.id }})}"
|
||||
@click='addFromCatalog({{ p.id }}, {{ p.name|tojson }}, {{ p.selling_price }})'
|
||||
:class="{selected: basket.some((i) => i.priceId === {{ price.id }})}"
|
||||
@click='addFromCatalog({{ price.id }}, {{ price.full_label|tojson }}, {{ price.amount }})'
|
||||
>
|
||||
{% if p.icon %}
|
||||
{% if price.product.icon %}
|
||||
<img
|
||||
class="card-image"
|
||||
src="{{ p.icon.url }}"
|
||||
alt="image de {{ p.name }}"
|
||||
src="{{ price.product.icon.url }}"
|
||||
alt="image de {{ price.full_label }}"
|
||||
>
|
||||
{% else %}
|
||||
<i class="fa-regular fa-image fa-2x card-image"></i>
|
||||
{% endif %}
|
||||
<div class="card-content">
|
||||
<h4 class="card-title">{{ p.name }}</h4>
|
||||
<p>{{ p.selling_price }} €</p>
|
||||
<h4 class="card-title">{{ price.full_label }}</h4>
|
||||
<p>{{ price.amount }} €</p>
|
||||
</div>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>{% trans %}There are no items available for sale{% endtrans %}</p>
|
||||
{% endfor %}
|
||||
|
||||
@@ -11,7 +11,12 @@ from pytest_django.asserts import assertRedirects
|
||||
|
||||
from core.baker_recipes import subscriber_user
|
||||
from core.models import Group, User
|
||||
from counter.baker_recipes import product_recipe, refill_recipe, sale_recipe
|
||||
from counter.baker_recipes import (
|
||||
price_recipe,
|
||||
product_recipe,
|
||||
refill_recipe,
|
||||
sale_recipe,
|
||||
)
|
||||
from counter.models import (
|
||||
Counter,
|
||||
Customer,
|
||||
@@ -147,29 +152,29 @@ class TestEboutic(TestCase):
|
||||
|
||||
product_type = baker.make(ProductType)
|
||||
|
||||
cls.snack = product_recipe.make(
|
||||
selling_price=1.5, special_selling_price=1, product_type=product_type
|
||||
cls.snack = price_recipe.make(
|
||||
amount=1.5, product=product_recipe.make(product_type=product_type)
|
||||
)
|
||||
cls.beer = product_recipe.make(
|
||||
limit_age=18,
|
||||
selling_price=2.5,
|
||||
special_selling_price=1,
|
||||
product_type=product_type,
|
||||
cls.beer = price_recipe.make(
|
||||
product=product_recipe.make(limit_age=18, product_type=product_type),
|
||||
amount=2.5,
|
||||
)
|
||||
cls.not_in_counter = product_recipe.make(
|
||||
selling_price=3.5, product_type=product_type
|
||||
cls.not_in_counter = price_recipe.make(
|
||||
product=product_recipe.make(product_type=product_type), amount=3.5
|
||||
)
|
||||
cls.cotiz = price_recipe.make(
|
||||
amount=10, product=product_recipe.make(product_type=product_type)
|
||||
)
|
||||
cls.cotiz = product_recipe.make(selling_price=10, product_type=product_type)
|
||||
|
||||
cls.group_public.products.add(cls.snack, cls.beer, cls.not_in_counter)
|
||||
cls.group_cotiz.products.add(cls.cotiz)
|
||||
cls.group_public.prices.add(cls.snack, cls.beer, cls.not_in_counter)
|
||||
cls.group_cotiz.prices.add(cls.cotiz)
|
||||
|
||||
cls.subscriber.groups.add(cls.group_cotiz, cls.group_public)
|
||||
cls.new_customer.groups.add(cls.group_public)
|
||||
cls.new_customer_adult.groups.add(cls.group_public)
|
||||
|
||||
cls.eboutic = get_eboutic()
|
||||
cls.eboutic.products.add(cls.cotiz, cls.beer, cls.snack)
|
||||
cls.eboutic.products.add(cls.cotiz.product, cls.beer.product, cls.snack.product)
|
||||
|
||||
@classmethod
|
||||
def set_age(cls, user: User, age: int):
|
||||
@@ -253,7 +258,7 @@ class TestEboutic(TestCase):
|
||||
self.submit_basket([BasketItem(self.snack.id, 2)]),
|
||||
reverse("eboutic:checkout", kwargs={"basket_id": 1}),
|
||||
)
|
||||
assert Basket.objects.get(id=1).total == self.snack.selling_price * 2
|
||||
assert Basket.objects.get(id=1).total == self.snack.amount * 2
|
||||
|
||||
self.client.force_login(self.new_customer_adult)
|
||||
assertRedirects(
|
||||
@@ -263,8 +268,7 @@ class TestEboutic(TestCase):
|
||||
reverse("eboutic:checkout", kwargs={"basket_id": 2}),
|
||||
)
|
||||
assert (
|
||||
Basket.objects.get(id=2).total
|
||||
== self.snack.selling_price * 2 + self.beer.selling_price
|
||||
Basket.objects.get(id=2).total == self.snack.amount * 2 + self.beer.amount
|
||||
)
|
||||
|
||||
self.client.force_login(self.subscriber)
|
||||
@@ -280,7 +284,5 @@ class TestEboutic(TestCase):
|
||||
)
|
||||
assert (
|
||||
Basket.objects.get(id=3).total
|
||||
== self.snack.selling_price * 2
|
||||
+ self.beer.selling_price
|
||||
+ self.cotiz.selling_price
|
||||
== self.snack.amount * 2 + self.beer.amount + self.cotiz.amount
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ from model_bakery import baker
|
||||
from pytest_django.asserts import assertRedirects
|
||||
|
||||
from core.baker_recipes import old_subscriber_user, subscriber_user
|
||||
from counter.baker_recipes import product_recipe
|
||||
from counter.baker_recipes import price_recipe, product_recipe
|
||||
from counter.models import Product, ProductType, Selling
|
||||
from counter.tests.test_counter import force_refill_user
|
||||
from eboutic.models import Basket, BasketItem
|
||||
@@ -32,23 +32,22 @@ class TestPaymentBase(TestCase):
|
||||
cls.basket = baker.make(Basket, user=cls.customer)
|
||||
cls.refilling = product_recipe.make(
|
||||
product_type_id=settings.SITH_COUNTER_PRODUCTTYPE_REFILLING,
|
||||
selling_price=15,
|
||||
prices=[price_recipe.make(amount=15)],
|
||||
)
|
||||
|
||||
product_type = baker.make(ProductType)
|
||||
|
||||
cls.snack = product_recipe.make(
|
||||
selling_price=1.5, special_selling_price=1, product_type=product_type
|
||||
product_type=product_type, prices=[price_recipe.make(amount=1.5)]
|
||||
)
|
||||
cls.beer = product_recipe.make(
|
||||
limit_age=18,
|
||||
selling_price=2.5,
|
||||
special_selling_price=1,
|
||||
product_type=product_type,
|
||||
prices=[price_recipe.make(amount=2.5)],
|
||||
)
|
||||
|
||||
BasketItem.from_product(cls.snack, 1, cls.basket).save()
|
||||
BasketItem.from_product(cls.beer, 2, cls.basket).save()
|
||||
BasketItem.from_price(cls.snack.prices.first(), 1, cls.basket).save()
|
||||
BasketItem.from_price(cls.beer.prices.first(), 2, cls.basket).save()
|
||||
|
||||
|
||||
class TestPaymentSith(TestPaymentBase):
|
||||
@@ -116,13 +115,13 @@ class TestPaymentSith(TestPaymentBase):
|
||||
assert len(sellings) == 2
|
||||
assert sellings[0].payment_method == Selling.PaymentMethod.SITH_ACCOUNT
|
||||
assert sellings[0].quantity == 1
|
||||
assert sellings[0].unit_price == self.snack.selling_price
|
||||
assert sellings[0].unit_price == self.snack.prices.first().amount
|
||||
assert sellings[0].counter.type == "EBOUTIC"
|
||||
assert sellings[0].product == self.snack
|
||||
|
||||
assert sellings[1].payment_method == Selling.PaymentMethod.SITH_ACCOUNT
|
||||
assert sellings[1].quantity == 2
|
||||
assert sellings[1].unit_price == self.beer.selling_price
|
||||
assert sellings[1].unit_price == self.beer.prices.first().amount
|
||||
assert sellings[1].counter.type == "EBOUTIC"
|
||||
assert sellings[1].product == self.beer
|
||||
|
||||
@@ -146,7 +145,7 @@ class TestPaymentSith(TestPaymentBase):
|
||||
)
|
||||
|
||||
def test_refilling_in_basket(self):
|
||||
BasketItem.from_product(self.refilling, 1, self.basket).save()
|
||||
BasketItem.from_price(self.refilling.prices.first(), 1, self.basket).save()
|
||||
self.client.force_login(self.customer)
|
||||
force_refill_user(self.customer, self.basket.total + 1)
|
||||
self.customer.customer.refresh_from_db()
|
||||
@@ -191,8 +190,8 @@ class TestPaymentCard(TestPaymentBase):
|
||||
def test_buy_success(self):
|
||||
response = self.client.get(self.generate_bank_valid_answer(self.basket))
|
||||
assert response.status_code == 200
|
||||
assert response.content.decode("utf-8") == "Payment successful"
|
||||
assert Basket.objects.filter(id=self.basket.id).first() is None
|
||||
assert response.content.decode() == "Payment successful"
|
||||
assert not Basket.objects.filter(id=self.basket.id).exists()
|
||||
|
||||
sellings = Selling.objects.filter(customer=self.customer.customer).order_by(
|
||||
"quantity"
|
||||
@@ -200,13 +199,13 @@ class TestPaymentCard(TestPaymentBase):
|
||||
assert len(sellings) == 2
|
||||
assert sellings[0].payment_method == Selling.PaymentMethod.CARD
|
||||
assert sellings[0].quantity == 1
|
||||
assert sellings[0].unit_price == self.snack.selling_price
|
||||
assert sellings[0].unit_price == self.snack.prices.first().amount
|
||||
assert sellings[0].counter.type == "EBOUTIC"
|
||||
assert sellings[0].product == self.snack
|
||||
|
||||
assert sellings[1].payment_method == Selling.PaymentMethod.CARD
|
||||
assert sellings[1].quantity == 2
|
||||
assert sellings[1].unit_price == self.beer.selling_price
|
||||
assert sellings[1].unit_price == self.beer.prices.first().amount
|
||||
assert sellings[1].counter.type == "EBOUTIC"
|
||||
assert sellings[1].product == self.beer
|
||||
|
||||
@@ -216,7 +215,9 @@ class TestPaymentCard(TestPaymentBase):
|
||||
assert not customer.subscriptions.first().is_valid_now()
|
||||
|
||||
basket = baker.make(Basket, user=customer)
|
||||
BasketItem.from_product(Product.objects.get(code="2SCOTIZ"), 1, basket).save()
|
||||
BasketItem.from_price(
|
||||
Product.objects.get(code="2SCOTIZ").prices.first(), 1, basket
|
||||
).save()
|
||||
response = self.client.get(self.generate_bank_valid_answer(basket))
|
||||
assert response.status_code == 200
|
||||
|
||||
@@ -228,12 +229,13 @@ class TestPaymentCard(TestPaymentBase):
|
||||
assert subscription.location == "EBOUTIC"
|
||||
|
||||
def test_buy_refilling(self):
|
||||
BasketItem.from_product(self.refilling, 2, self.basket).save()
|
||||
price = self.refilling.prices.first()
|
||||
BasketItem.from_price(price, 2, self.basket).save()
|
||||
response = self.client.get(self.generate_bank_valid_answer(self.basket))
|
||||
assert response.status_code == 200
|
||||
|
||||
self.customer.customer.refresh_from_db()
|
||||
assert self.customer.customer.amount == self.refilling.selling_price * 2
|
||||
assert self.customer.customer.amount == price.amount * 2
|
||||
|
||||
def test_multiple_responses(self):
|
||||
bank_response = self.generate_bank_valid_answer(self.basket)
|
||||
@@ -253,17 +255,17 @@ class TestPaymentCard(TestPaymentBase):
|
||||
self.basket.delete()
|
||||
response = self.client.get(bank_response)
|
||||
assert response.status_code == 500
|
||||
assert (
|
||||
response.text
|
||||
== "Basket processing failed with error: SuspiciousOperation('Basket does not exists')"
|
||||
assert response.text == (
|
||||
"Basket processing failed with error: "
|
||||
"SuspiciousOperation('Basket does not exists')"
|
||||
)
|
||||
|
||||
def test_altered_basket(self):
|
||||
bank_response = self.generate_bank_valid_answer(self.basket)
|
||||
BasketItem.from_product(self.snack, 1, self.basket).save()
|
||||
BasketItem.from_price(self.snack.prices.first(), 1, self.basket).save()
|
||||
response = self.client.get(bank_response)
|
||||
assert response.status_code == 500
|
||||
assert (
|
||||
response.text == "Basket processing failed with error: "
|
||||
assert response.text == (
|
||||
"Basket processing failed with error: "
|
||||
"SuspiciousOperation('Basket total and amount do not match')"
|
||||
)
|
||||
|
||||
+40
-40
@@ -17,6 +17,7 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import contextlib
|
||||
import itertools
|
||||
import json
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -28,9 +29,7 @@ from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import (
|
||||
LoginRequiredMixin,
|
||||
)
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.core.exceptions import SuspiciousOperation, ValidationError
|
||||
from django.db import DatabaseError, transaction
|
||||
@@ -48,23 +47,16 @@ from django_countries.fields import Country
|
||||
|
||||
from core.auth.mixins import CanViewMixin, IsSubscriberMixin
|
||||
from core.views.mixins import FragmentMixin, UseFragmentsMixin
|
||||
from counter.forms import BaseBasketForm, BasketProductForm, BillingInfoForm
|
||||
from counter.forms import BaseBasketForm, BasketItemForm, BillingInfoForm
|
||||
from counter.models import (
|
||||
BillingInfo,
|
||||
Customer,
|
||||
Product,
|
||||
Price,
|
||||
Refilling,
|
||||
Selling,
|
||||
get_eboutic,
|
||||
)
|
||||
from eboutic.models import (
|
||||
Basket,
|
||||
BasketItem,
|
||||
BillingInfoState,
|
||||
Invoice,
|
||||
InvoiceItem,
|
||||
get_eboutic_products,
|
||||
)
|
||||
from eboutic.models import Basket, BasketItem, BillingInfoState, Invoice, InvoiceItem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
|
||||
@@ -78,7 +70,7 @@ class BaseEbouticBasketForm(BaseBasketForm):
|
||||
|
||||
|
||||
EbouticBasketForm = forms.formset_factory(
|
||||
BasketProductForm, formset=BaseEbouticBasketForm, absolute_max=None, min_num=1
|
||||
BasketItemForm, formset=BaseEbouticBasketForm, absolute_max=None, min_num=1
|
||||
)
|
||||
|
||||
|
||||
@@ -88,7 +80,6 @@ class EbouticMainView(LoginRequiredMixin, FormView):
|
||||
The purchasable products are those of the eboutic which
|
||||
belong to a category of products of a product category
|
||||
(orphan products are inaccessible).
|
||||
|
||||
"""
|
||||
|
||||
template_name = "eboutic/eboutic_main.jinja"
|
||||
@@ -99,7 +90,7 @@ class EbouticMainView(LoginRequiredMixin, FormView):
|
||||
kwargs["form_kwargs"] = {
|
||||
"customer": self.customer,
|
||||
"counter": get_eboutic(),
|
||||
"allowed_products": {product.id: product for product in self.products},
|
||||
"allowed_prices": {price.id: price for price in self.prices},
|
||||
}
|
||||
return kwargs
|
||||
|
||||
@@ -110,19 +101,25 @@ class EbouticMainView(LoginRequiredMixin, FormView):
|
||||
|
||||
with transaction.atomic():
|
||||
self.basket = Basket.objects.create(user=self.request.user)
|
||||
for form in formset:
|
||||
BasketItem.from_product(
|
||||
form.product, form.cleaned_data["quantity"], self.basket
|
||||
).save()
|
||||
self.basket.save()
|
||||
BasketItem.objects.bulk_create(
|
||||
[
|
||||
BasketItem.from_price(
|
||||
form.price, form.cleaned_data["quantity"], self.basket
|
||||
)
|
||||
for form in formset
|
||||
]
|
||||
)
|
||||
return super().form_valid(formset)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("eboutic:checkout", kwargs={"basket_id": self.basket.id})
|
||||
|
||||
@cached_property
|
||||
def products(self) -> list[Product]:
|
||||
return get_eboutic_products(self.request.user)
|
||||
def prices(self) -> list[Price]:
|
||||
return get_eboutic().get_prices_for(
|
||||
self.customer,
|
||||
order_by=["product__product_type__order", "product_id", "amount"],
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def customer(self) -> Customer:
|
||||
@@ -130,7 +127,12 @@ class EbouticMainView(LoginRequiredMixin, FormView):
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["products"] = self.products
|
||||
context["categories"] = [
|
||||
list(i[1])
|
||||
for i in itertools.groupby(
|
||||
self.prices, key=lambda p: p.product.product_type_id
|
||||
)
|
||||
]
|
||||
context["customer_amount"] = self.request.user.account_balance
|
||||
|
||||
purchases = (
|
||||
@@ -267,11 +269,8 @@ class EbouticPayWithSith(CanViewMixin, SingleObjectMixin, View):
|
||||
def post(self, request, *args, **kwargs):
|
||||
basket = self.get_object()
|
||||
refilling = settings.SITH_COUNTER_PRODUCTTYPE_REFILLING
|
||||
if basket.items.filter(type_id=refilling).exists():
|
||||
messages.error(
|
||||
self.request,
|
||||
_("You can't buy a refilling with sith money"),
|
||||
)
|
||||
if basket.items.filter(product__product_type_id=refilling).exists():
|
||||
messages.error(self.request, _("You can't buy a refilling with sith money"))
|
||||
return redirect("eboutic:payment_result", "failure")
|
||||
|
||||
eboutic = get_eboutic()
|
||||
@@ -326,22 +325,23 @@ class EtransactionAutoAnswer(View):
|
||||
raise SuspiciousOperation(
|
||||
"Basket total and amount do not match"
|
||||
)
|
||||
i = Invoice()
|
||||
i.user = b.user
|
||||
i.payment_method = "CARD"
|
||||
i.save()
|
||||
for it in b.items.all():
|
||||
i = Invoice.objects.create(user=b.user)
|
||||
InvoiceItem.objects.bulk_create(
|
||||
[
|
||||
InvoiceItem(
|
||||
invoice=i,
|
||||
product_id=it.product_id,
|
||||
product_name=it.product_name,
|
||||
type_id=it.type_id,
|
||||
product_unit_price=it.product_unit_price,
|
||||
quantity=it.quantity,
|
||||
).save()
|
||||
product_id=item.product_id,
|
||||
label=item.label,
|
||||
unit_price=item.unit_price,
|
||||
quantity=item.quantity,
|
||||
)
|
||||
for item in b.items.all()
|
||||
]
|
||||
)
|
||||
i.validate()
|
||||
b.delete()
|
||||
except Exception as e:
|
||||
sentry_sdk.capture_exception(e)
|
||||
return HttpResponse(
|
||||
"Basket processing failed with error: " + repr(e), status=500
|
||||
)
|
||||
|
||||
@@ -56,7 +56,7 @@ msgstr "nom"
|
||||
msgid "owner"
|
||||
msgstr "propriétaire"
|
||||
|
||||
#: api/models.py core/models.py
|
||||
#: api/models.py core/models.py counter/models.py
|
||||
msgid "groups"
|
||||
msgstr "groupes"
|
||||
|
||||
@@ -3043,24 +3043,6 @@ msgstr ""
|
||||
"Décrivez le produit. Si c'est un click pour un évènement, donnez quelques "
|
||||
"détails dessus, comme la date (en incluant l'année)."
|
||||
|
||||
#: counter/forms.py
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This product is a formula. Its price cannot be greater than the price of the "
|
||||
"products constituting it, which is %(price)s €"
|
||||
msgstr ""
|
||||
"Ce produit est une formule. Son prix ne peut pas être supérieur au prix des "
|
||||
"produits qui la constituent, soit %(price)s €."
|
||||
|
||||
#: counter/forms.py
|
||||
#, python-format
|
||||
msgid ""
|
||||
"This product is a formula. Its special price cannot be greater than the "
|
||||
"price of the products constituting it, which is %(price)s €"
|
||||
msgstr ""
|
||||
"Ce produit est une formule. Son prix spécial ne peut pas être supérieur au "
|
||||
"prix des produits qui la constituent, soit %(price)s €."
|
||||
|
||||
#: counter/forms.py
|
||||
msgid ""
|
||||
"The same product cannot be at the same time the result and a part of the "
|
||||
@@ -3069,19 +3051,13 @@ msgstr ""
|
||||
"Un même produit ne peut pas être à la fois le résultat et un élément de la "
|
||||
"formule."
|
||||
|
||||
#: counter/forms.py
|
||||
msgid ""
|
||||
"The result cannot be more expensive than the total of the other products."
|
||||
msgstr ""
|
||||
"Le résultat ne peut pas être plus cher que le total des autres produits."
|
||||
|
||||
#: counter/forms.py
|
||||
msgid "Refound this account"
|
||||
msgstr "Rembourser ce compte"
|
||||
|
||||
#: counter/forms.py
|
||||
msgid "The selected product isn't available for this user"
|
||||
msgstr "Le produit sélectionné n'est pas disponnible pour cet utilisateur"
|
||||
msgstr "Le produit sélectionné n'est pas disponible pour cet utilisateur"
|
||||
|
||||
#: counter/forms.py
|
||||
msgid "Submitted basket is invalid"
|
||||
@@ -3195,18 +3171,6 @@ msgstr "prix d'achat"
|
||||
msgid "Initial cost of purchasing the product"
|
||||
msgstr "Coût initial d'achat du produit"
|
||||
|
||||
#: counter/models.py
|
||||
msgid "selling price"
|
||||
msgstr "prix de vente"
|
||||
|
||||
#: counter/models.py
|
||||
msgid "special selling price"
|
||||
msgstr "prix de vente spécial"
|
||||
|
||||
#: counter/models.py
|
||||
msgid "Price for barmen during their permanence"
|
||||
msgstr "Prix pour les barmen durant leur permanence"
|
||||
|
||||
#: counter/models.py
|
||||
msgid "icon"
|
||||
msgstr "icône"
|
||||
@@ -3219,6 +3183,10 @@ msgstr "âge limite"
|
||||
msgid "tray price"
|
||||
msgstr "prix plateau"
|
||||
|
||||
#: counter/models.py
|
||||
msgid "Buy five, get the sixth free"
|
||||
msgstr "Pour cinq achetés, le sixième offert"
|
||||
|
||||
#: counter/models.py
|
||||
msgid "buying groups"
|
||||
msgstr "groupe d'achat"
|
||||
@@ -3231,10 +3199,35 @@ msgstr "archivé"
|
||||
msgid "updated at"
|
||||
msgstr "mis à jour le"
|
||||
|
||||
#: counter/models.py
|
||||
#: counter/models.py eboutic/models.py
|
||||
msgid "product"
|
||||
msgstr "produit"
|
||||
|
||||
#: counter/models.py
|
||||
msgid "always show"
|
||||
msgstr "toujours montrer"
|
||||
|
||||
#: counter/models.py
|
||||
msgid ""
|
||||
"If this option is enabled, people will see this price and be able to pay it, "
|
||||
"even if another cheaper price exists. Else it will visible only if it is the "
|
||||
"cheapest available price."
|
||||
msgstr ""
|
||||
"Si cette option est activée, les gens verront ce prix et pourront le payer, "
|
||||
"même si un autre moins cher existe. Dans le cas contraire, le prix sera "
|
||||
"visible uniquement s'il s'agit du prix disponible le plus faible."
|
||||
|
||||
#: counter/models.py
|
||||
msgid ""
|
||||
"A short label for easier differentiation if a user can see multiple prices."
|
||||
msgstr ""
|
||||
"Un court libellé pour faciliter la différentiation si un utilisateur peut "
|
||||
"voir plusieurs prix."
|
||||
|
||||
#: counter/models.py
|
||||
msgid "price"
|
||||
msgstr "prix"
|
||||
|
||||
#: counter/models.py
|
||||
msgid "products"
|
||||
msgstr "produits"
|
||||
@@ -3713,10 +3706,6 @@ msgstr ""
|
||||
msgid "New formula"
|
||||
msgstr "Nouvelle formule"
|
||||
|
||||
#: counter/templates/counter/formula_list.jinja
|
||||
msgid "instead of"
|
||||
msgstr "au lieu de"
|
||||
|
||||
#: counter/templates/counter/fragments/create_student_card.jinja
|
||||
msgid "No student card registered."
|
||||
msgstr "Aucune carte étudiante enregistrée."
|
||||
@@ -3839,6 +3828,10 @@ msgstr ""
|
||||
"votre cotisation. Si vous ne renouvelez pas votre cotisation, il n'y aura "
|
||||
"aucune conséquence autre que le retrait de l'argent de votre compte."
|
||||
|
||||
#: counter/templates/counter/product_form.jinja
|
||||
msgid "Remove price"
|
||||
msgstr "Retirer le prix"
|
||||
|
||||
#: counter/templates/counter/product_form.jinja
|
||||
msgid "Remove this action"
|
||||
msgstr "Retirer cette action"
|
||||
@@ -3860,6 +3853,14 @@ msgstr "Dernière mise à jour"
|
||||
msgid "Product creation"
|
||||
msgstr "Création de produit"
|
||||
|
||||
#: counter/templates/counter/product_form.jinja
|
||||
msgid "Prices"
|
||||
msgstr "Prix"
|
||||
|
||||
#: counter/templates/counter/product_form.jinja
|
||||
msgid "Add a price"
|
||||
msgstr "Ajouter un prix"
|
||||
|
||||
#: counter/templates/counter/product_form.jinja
|
||||
msgid "Automatic actions"
|
||||
msgstr "Actions automatiques"
|
||||
@@ -4111,18 +4112,10 @@ msgstr "validé"
|
||||
msgid "Invoice already validated"
|
||||
msgstr "Facture déjà validée"
|
||||
|
||||
#: eboutic/models.py
|
||||
msgid "product id"
|
||||
msgstr "ID du produit"
|
||||
|
||||
#: eboutic/models.py
|
||||
msgid "product name"
|
||||
msgstr "nom du produit"
|
||||
|
||||
#: eboutic/models.py
|
||||
msgid "product type id"
|
||||
msgstr "id du type du produit"
|
||||
|
||||
#: eboutic/models.py
|
||||
msgid "basket"
|
||||
msgstr "panier"
|
||||
|
||||
Generated
+351
-289
File diff suppressed because it is too large
Load Diff
+13
-13
@@ -25,9 +25,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.29.0",
|
||||
"@babel/preset-env": "^7.29.0",
|
||||
"@biomejs/biome": "^2.4.6",
|
||||
"@hey-api/openapi-ts": "^0.94.0",
|
||||
"@babel/preset-env": "^7.29.2",
|
||||
"@biomejs/biome": "^2.4.13",
|
||||
"@hey-api/openapi-ts": "^0.94.5",
|
||||
"@rollup/plugin-inject": "^5.0.5",
|
||||
"@types/alpinejs": "^3.13.11",
|
||||
"@types/cytoscape-cxtmenu": "^3.4.5",
|
||||
@@ -35,10 +35,10 @@
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"rollup-plugin-visualizer": "^7.0.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0"
|
||||
"vite": "^8.0.10"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alpinejs/sort": "^3.15.8",
|
||||
"@alpinejs/sort": "^3.15.11",
|
||||
"@arendjr/text-clipper": "npm:@jsr/arendjr__text-clipper@^3.0.0",
|
||||
"@floating-ui/dom": "^1.7.6",
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
@@ -46,25 +46,25 @@
|
||||
"@fullcalendar/daygrid": "^6.1.20",
|
||||
"@fullcalendar/icalendar": "^6.1.20",
|
||||
"@fullcalendar/list": "^6.1.20",
|
||||
"@sentry/browser": "^10.43.0",
|
||||
"@zip.js/zip.js": "^2.8.23",
|
||||
"3d-force-graph": "^1.79.1",
|
||||
"alpinejs": "^3.15.8",
|
||||
"@sentry/browser": "^10.51.0",
|
||||
"@zip.js/zip.js": "^2.8.26",
|
||||
"3d-force-graph": "^1.80.0",
|
||||
"alpinejs": "^3.15.11",
|
||||
"chart.js": "^4.5.1",
|
||||
"country-flag-emoji-polyfill": "^0.1.8",
|
||||
"cytoscape": "^3.33.1",
|
||||
"cytoscape": "^3.33.2",
|
||||
"cytoscape-cxtmenu": "^3.5.0",
|
||||
"cytoscape-klay": "^3.1.4",
|
||||
"d3-force-3d": "^3.0.6",
|
||||
"easymde": "^2.20.0",
|
||||
"glob": "^13.0.6",
|
||||
"html2canvas": "^1.4.1",
|
||||
"htmx.org": "^2.0.8",
|
||||
"htmx.org": "^2.0.10",
|
||||
"js-cookie": "^3.0.5",
|
||||
"lit-html": "^3.3.2",
|
||||
"native-file-system-adapter": "^3.0.1",
|
||||
"three": "^0.183.2",
|
||||
"three": "^0.184.0",
|
||||
"three-spritetext": "^1.10.0",
|
||||
"tom-select": "^2.5.2"
|
||||
"tom-select": "^2.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
+27
-27
@@ -20,32 +20,32 @@ license = { text = "GPL-3.0-only" }
|
||||
requires-python = "<4.0,>=3.12"
|
||||
dependencies = [
|
||||
"django>=5.2.13,<6.0.0",
|
||||
"django-ninja>=1.5.3,<6.0.0",
|
||||
"django-ninja-extra>=0.31.0",
|
||||
"Pillow>=12.1.1,<13.0.0",
|
||||
"django-ninja>=1.6.2,<6.0.0",
|
||||
"django-ninja-extra>=0.31.4",
|
||||
"Pillow>=12.2.0,<13.0.0",
|
||||
"mistune>=3.2.0,<4.0.0",
|
||||
"django-jinja<3.0.0,>=2.11.0",
|
||||
"cryptography>=46.0.5,<47.0.0",
|
||||
"cryptography>=47.0.0,<48.0.0",
|
||||
"django-phonenumber-field>=8.4.0,<9.0.0",
|
||||
"phonenumbers>=9.0.25,<10.0.0",
|
||||
"reportlab>=4.4.10,<5.0.0",
|
||||
"django-haystack<4.0.0,>=3.3.0",
|
||||
"xapian-haystack<3.1.0,<5.0.0",
|
||||
"phonenumbers>=9.0.29,<10.0.0",
|
||||
"reportlab>=4.5.0,<5.0.0",
|
||||
"django-haystack>=3.3.0,<4.0.0",
|
||||
"xapian-haystack>=4.0.0,<5.0.0",
|
||||
"libsass<1.0.0,>=0.23.0",
|
||||
"django-ordered-model<4.0.0,>=3.7.4",
|
||||
"django-simple-captcha<1.0.0,>=0.6.3",
|
||||
"python-dateutil<3.0.0.0,>=2.9.0.post0",
|
||||
"sentry-sdk>=2.54.0,<3.0.0",
|
||||
"sentry-sdk>=2.58.0,<3.0.0",
|
||||
"jinja2<4.0.0,>=3.1.6",
|
||||
"django-countries>=8.2.0,<9.0.0",
|
||||
"dict2xml>=1.7.8,<2.0.0",
|
||||
"Sphinx<6,>=5",
|
||||
"tomli>=2.4.0,<3.0.0",
|
||||
"tomli>=2.4.1,<3.0.0",
|
||||
"django-honeypot>=1.3.0,<2",
|
||||
"pydantic-extra-types>=2.11.0,<3.0.0",
|
||||
"ical>=11.1.0,<12",
|
||||
"redis[hiredis]>=5.3.0,<8.0.0",
|
||||
"environs[django]>=14.5.0,<16.0.0",
|
||||
"pydantic-extra-types>=2.11.1,<3.0.0",
|
||||
"ical>=11.1.0,<14.0.0",
|
||||
"redis[hiredis]>=6.4.0,<8.0.0",
|
||||
"environs[django]>=15.0.1,<16.0.0",
|
||||
"requests>=2.32.5,<3.0.0",
|
||||
"honcho>=2.0.0",
|
||||
"psutil>=7.2.2,<8.0.0",
|
||||
@@ -63,29 +63,29 @@ prod = [
|
||||
"psycopg[c]>=3.3.3,<4.0.0",
|
||||
]
|
||||
dev = [
|
||||
"django-debug-toolbar>=6.2.0,<7",
|
||||
"ipython>=9.11.0,<10.0.0",
|
||||
"pre-commit>=4.5.1,<5.0.0",
|
||||
"ruff>=0.15.5,<1.0.0",
|
||||
"djhtml>=3.0.10,<4.0.0",
|
||||
"faker>=40.8.0,<41.0.0",
|
||||
"django-debug-toolbar>=6.3.0,<7",
|
||||
"ipython>=9.13.0,<10.0.0",
|
||||
"pre-commit>=4.6.0,<5.0.0",
|
||||
"ruff>=0.15.12,<1.0.0",
|
||||
"djhtml>=3.0.11,<4.0.0",
|
||||
"faker>=40.15.0,<41.0.0",
|
||||
"rjsmin>=1.2.5,<2.0.0",
|
||||
]
|
||||
tests = [
|
||||
"freezegun>=1.5.5,<2.0.0",
|
||||
"pytest>=9.0.2,<10.0.0",
|
||||
"pytest-cov>=7.0.0,<8.0.0",
|
||||
"pytest>=9.0.3,<10.0.0",
|
||||
"pytest-cov>=7.1.0,<8.0.0",
|
||||
"pytest-django<5.0.0,>=4.12.0",
|
||||
"model-bakery<2.0.0,>=1.23.3",
|
||||
"model-bakery<2.0.0,>=1.23.4",
|
||||
"beautifulsoup4>=4.14.3,<5",
|
||||
"lxml>=6.0.2,<7",
|
||||
"lxml>=6.1.0,<7",
|
||||
]
|
||||
docs = [
|
||||
"mkdocs<2.0.0,>=1.6.1",
|
||||
"mkdocs-material>=9.7.5,<10.0.0",
|
||||
"mkdocstrings>=1.0.3,<2.0.0",
|
||||
"mkdocs-material>=9.7.6,<10.0.0",
|
||||
"mkdocstrings>=1.0.4,<2.0.0",
|
||||
"mkdocstrings-python>=2.0.3,<3.0.0",
|
||||
"mkdocs-include-markdown-plugin>=7.2.1,<8.0.0",
|
||||
"mkdocs-include-markdown-plugin>=7.2.2,<8.0.0",
|
||||
]
|
||||
|
||||
[tool.uv]
|
||||
|
||||
@@ -33,8 +33,6 @@ class TestMergeUser(TestCase):
|
||||
cls.club = baker.make(Club)
|
||||
cls.eboutic = Counter.objects.get(name="Eboutic")
|
||||
cls.barbar = Product.objects.get(code="BARB")
|
||||
cls.barbar.selling_price = 2
|
||||
cls.barbar.save()
|
||||
cls.root = User.objects.get(username="root")
|
||||
cls.to_keep = User.objects.create(
|
||||
username="to_keep", password="plop", email="u.1@utbm.fr"
|
||||
|
||||
+14
-1
@@ -1,7 +1,9 @@
|
||||
import datetime
|
||||
from typing import Any
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.timezone import get_current_timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import User
|
||||
@@ -50,13 +52,24 @@ class AlbumEditForm(forms.ModelForm):
|
||||
model = Album
|
||||
fields = ["name", "date", "file", "parent", "edit_groups"]
|
||||
widgets = {
|
||||
"parent": AutoCompleteSelectAlbum,
|
||||
"edit_groups": AutoCompleteSelectMultipleGroup,
|
||||
}
|
||||
|
||||
name = forms.CharField(max_length=Album.NAME_MAX_LENGTH, label=_("file name"))
|
||||
date = forms.DateField(label=_("Date"), widget=SelectDate, required=True)
|
||||
recursive = forms.BooleanField(label=_("Apply rights recursively"), required=False)
|
||||
parent = forms.ModelChoiceField(
|
||||
Album.objects.all(), required=True, widget=AutoCompleteSelectAlbum
|
||||
)
|
||||
|
||||
def clean_date(self):
|
||||
album_date: datetime.date = self.cleaned_data["date"]
|
||||
return datetime.datetime(
|
||||
year=album_date.year,
|
||||
month=album_date.month,
|
||||
day=album_date.day,
|
||||
tzinfo=get_current_timezone(),
|
||||
)
|
||||
|
||||
|
||||
class PictureModerationRequestForm(forms.ModelForm):
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
--loading-size: 20px
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
@media (min-width: 700px) and (max-width: 1000px) {
|
||||
max-width: calc(50% - 5px);
|
||||
}
|
||||
|
||||
@@ -201,57 +201,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
.general {
|
||||
#pict .general {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 20px;
|
||||
gap: 3em;
|
||||
justify-content: space-evenly;
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
gap: 1em;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
>.infos {
|
||||
.infos, .tools {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
gap: .5em;
|
||||
@media (min-width: 700px) {
|
||||
max-width: 350px;
|
||||
}
|
||||
}
|
||||
.infos > div, .tools > div > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .35em;
|
||||
}
|
||||
|
||||
>div>div {
|
||||
.tools > div, >.infos >div>div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
>*:first-child {
|
||||
min-width: 150px;
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
>.tools {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
|
||||
>div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
>div {
|
||||
>a.button {
|
||||
box-sizing: border-box;
|
||||
>div>div {
|
||||
>a.btn {
|
||||
background-color: $primary-neutral-light-color;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
padding: 0;
|
||||
color: black;
|
||||
border-radius: 5px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
|
||||
&:hover {
|
||||
background-color: #aaa;
|
||||
@@ -268,9 +263,9 @@
|
||||
|
||||
&.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,17 @@
|
||||
{% trans %}See all the photos taken during events organised by the AE.{% endtrans %}
|
||||
{%- endblock %}
|
||||
|
||||
{% block metatags %}
|
||||
<meta property="og:url" content="{{ request.build_absolute_uri() }}" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Stock à souvenirs" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Retrouvez toutes les photos prises durant les événements organisés par l'AE."
|
||||
/>
|
||||
<meta property="og:image" content="{{ request.build_absolute_uri(static("core/img/logo_no_text.png")) }}" />
|
||||
{% endblock %}
|
||||
|
||||
{% set is_sas_admin = user.is_root or user.is_in_group(pk=settings.SITH_GROUP_SAS_ADMIN_ID) %}
|
||||
|
||||
{% from "sas/macros.jinja" import display_album %}
|
||||
|
||||
@@ -118,15 +118,20 @@
|
||||
<a class="text" :href="currentPicture.full_size_url">
|
||||
{% trans %}HD version{% endtrans %}
|
||||
</a>
|
||||
<br>
|
||||
<a class="text danger " :href="currentPicture.report_url">
|
||||
{% trans %}Ask for removal{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<a class="button" :href="currentPicture.edit_url"><i class="fa-regular fa-pen-to-square edit-action"></i></a>
|
||||
<a class="button" href="?rotate_left"><i class="fa-solid fa-rotate-left"></i></a>
|
||||
<a class="button" href="?rotate_right"><i class="fa-solid fa-rotate-right"></i></a>
|
||||
<a
|
||||
class="btn btn-no-text"
|
||||
:href="currentPicture.edit_url"
|
||||
x-show="{{ user.has_perm("sas.change_sasfile")|tojson }} || currentPicture.owner.id === {{ user.id }}"
|
||||
>
|
||||
<i class="fa-regular fa-pen-to-square edit-action"></i>
|
||||
</a>
|
||||
<a class="btn btn-no-text" href="?rotate_left"><i class="fa-solid fa-rotate-left"></i></a>
|
||||
<a class="btn btn-no-text" href="?rotate_right"><i class="fa-solid fa-rotate-right"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,12 +20,14 @@ from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import localdate
|
||||
from model_bakery import baker
|
||||
from pytest_django.asserts import assertHTMLEqual, assertInHTML, assertRedirects
|
||||
|
||||
from core.baker_recipes import old_subscriber_user, subscriber_user
|
||||
from core.models import Group, User
|
||||
from sas.baker_recipes import picture_recipe
|
||||
from sas.forms import AlbumEditForm
|
||||
from sas.models import Album, Picture
|
||||
|
||||
# Create your tests here.
|
||||
@@ -64,6 +66,25 @@ def test_main_page_no_form_for_regular_users(client: Client):
|
||||
assert len(forms) == 0
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_main_page_displayed_albums(client: Client):
|
||||
"""Test that the right data is displayed on the SAS main page"""
|
||||
sas = Album.objects.get(id=settings.SITH_SAS_ROOT_DIR_ID)
|
||||
Album.objects.exclude(id=sas.id).delete()
|
||||
album_a = baker.make(Album, parent=sas, is_moderated=True)
|
||||
album_b = baker.make(Album, parent=album_a, is_moderated=True)
|
||||
album_c = baker.make(Album, parent=sas, is_moderated=True)
|
||||
baker.make(Album, parent=sas, is_moderated=False)
|
||||
client.force_login(subscriber_user.make())
|
||||
res = client.get(reverse("sas:main"))
|
||||
# album_b is not a direct child of the SAS, so it shouldn't be displayed
|
||||
# in the categories, but it should appear in the latest albums.
|
||||
# album_d isn't moderated, so it shouldn't appear at all for a simple user.
|
||||
# Also, the SAS itself shouldn't be listed in the albums.
|
||||
assert res.context_data["latest"] == [album_c, album_b, album_a]
|
||||
assert res.context_data["categories"] == [album_a, album_c]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_main_page_content_anonymous(client: Client):
|
||||
"""Test that public users see only an incentive to login"""
|
||||
@@ -89,6 +110,15 @@ def test_album_access_non_subscriber(client: Client):
|
||||
assert res.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_accessing_sas_from_album_view_is_404(client: Client):
|
||||
"""Test that trying to see the SAS with a regular album view isn't allowed."""
|
||||
res = client.get(
|
||||
reverse("sas:album", kwargs={"album_id": settings.SITH_SAS_ROOT_DIR_ID})
|
||||
)
|
||||
assert res.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestAlbumUpload:
|
||||
@staticmethod
|
||||
@@ -133,6 +163,140 @@ class TestAlbumUpload:
|
||||
assert not album.children.exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestAlbumEdit:
|
||||
@pytest.fixture
|
||||
def sas_root(self) -> Album:
|
||||
return Album.objects.get(id=settings.SITH_SAS_ROOT_DIR_ID)
|
||||
|
||||
@pytest.fixture
|
||||
def album(self) -> Album:
|
||||
return baker.make(
|
||||
Album, parent_id=settings.SITH_SAS_ROOT_DIR_ID, is_moderated=True
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user",
|
||||
[None, lambda: baker.make(User), subscriber_user.make],
|
||||
)
|
||||
def test_permission_denied(
|
||||
self,
|
||||
client: Client,
|
||||
album: Album,
|
||||
user: Callable[[], User] | None,
|
||||
):
|
||||
if user:
|
||||
client.force_login(user())
|
||||
|
||||
url = reverse("sas:album_edit", kwargs={"album_id": album.pk})
|
||||
response = client.get(url)
|
||||
assert response.status_code == 403
|
||||
response = client.post(url)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_sas_root_read_only(self, client: Client, sas_root: Album):
|
||||
moderator = baker.make(
|
||||
User, groups=[Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
|
||||
)
|
||||
client.force_login(moderator)
|
||||
url = reverse("sas:album_edit", kwargs={"album_id": sas_root.pk})
|
||||
response = client.get(url)
|
||||
assert response.status_code == 404
|
||||
response = client.post(url)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("excluded", "is_valid"),
|
||||
[
|
||||
("name", False),
|
||||
("date", False),
|
||||
("file", True),
|
||||
("parent", False),
|
||||
("edit_groups", True),
|
||||
("recursive", True),
|
||||
],
|
||||
)
|
||||
def test_form_required(self, album: Album, excluded: str, is_valid: bool): # noqa: FBT001
|
||||
data = {
|
||||
"name": album.name[: Album.NAME_MAX_LENGTH],
|
||||
"parent": baker.make(Album, parent=album.parent, is_moderated=True).pk,
|
||||
"date": localdate(),
|
||||
"file": "/random/path",
|
||||
"edit_groups": [settings.SITH_GROUP_SAS_ADMIN_ID],
|
||||
"recursive": False,
|
||||
}
|
||||
del data[excluded]
|
||||
assert AlbumEditForm(data=data).is_valid() == is_valid
|
||||
|
||||
def test_form_album_name(self, album: Album):
|
||||
data = {
|
||||
"name": album.name[: Album.NAME_MAX_LENGTH],
|
||||
"parent": album.pk,
|
||||
"date": localdate(),
|
||||
}
|
||||
assert AlbumEditForm(data=data).is_valid()
|
||||
|
||||
data["name"] = album.name[: Album.NAME_MAX_LENGTH + 1]
|
||||
assert not AlbumEditForm(data=data).is_valid()
|
||||
|
||||
def test_update_recursive_parent(self, client: Client, album: Album):
|
||||
client.force_login(baker.make(User, is_superuser=True))
|
||||
|
||||
payload = {
|
||||
"name": album.name[: Album.NAME_MAX_LENGTH],
|
||||
"parent": album.pk,
|
||||
"date": localdate(),
|
||||
}
|
||||
response = client.post(
|
||||
reverse("sas:album_edit", kwargs={"album_id": album.pk}), payload
|
||||
)
|
||||
assertInHTML("<li>Boucle dans l'arborescence des dossiers</li>", response.text)
|
||||
assert response.status_code == 200
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"user",
|
||||
[
|
||||
lambda: baker.make(User, is_superuser=True),
|
||||
lambda: baker.make(
|
||||
User, groups=[Group.objects.get(pk=settings.SITH_GROUP_SAS_ADMIN_ID)]
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"parent",
|
||||
[
|
||||
lambda: baker.make(
|
||||
Album, parent_id=settings.SITH_SAS_ROOT_DIR_ID, is_moderated=True
|
||||
),
|
||||
lambda: Album.objects.get(id=settings.SITH_SAS_ROOT_DIR_ID),
|
||||
],
|
||||
)
|
||||
def test_update(
|
||||
self,
|
||||
client: Client,
|
||||
album: Album,
|
||||
sas_root: Album,
|
||||
user: Callable[[], User],
|
||||
parent: Callable[[], Album],
|
||||
):
|
||||
client.force_login(user())
|
||||
expected_redirect = reverse("sas:album", kwargs={"album_id": album.pk})
|
||||
payload = {
|
||||
"name": album.name[: Album.NAME_MAX_LENGTH],
|
||||
"parent": parent().id,
|
||||
"date": localdate(),
|
||||
"recursive": False,
|
||||
}
|
||||
response = client.post(
|
||||
reverse("sas:album_edit", kwargs={"album_id": album.pk}), payload
|
||||
)
|
||||
assertRedirects(response, expected_redirect)
|
||||
album.refresh_from_db()
|
||||
assert album.name == payload["name"]
|
||||
assert album.parent.id == payload["parent"]
|
||||
assert localdate(album.date) == localdate()
|
||||
|
||||
|
||||
class TestSasModeration(TestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
+7
-1
@@ -85,7 +85,9 @@ class SASMainView(UseFragmentsMixin, TemplateView):
|
||||
kwargs["categories"] = list(
|
||||
albums_qs.filter(parent_id=settings.SITH_SAS_ROOT_DIR_ID).order_by("id")
|
||||
)
|
||||
kwargs["latest"] = list(albums_qs.order_by("-id")[:5])
|
||||
kwargs["latest"] = list(
|
||||
albums_qs.exclude(id=settings.SITH_SAS_ROOT_DIR_ID).order_by("-id")[:5]
|
||||
)
|
||||
return kwargs
|
||||
|
||||
|
||||
@@ -126,6 +128,9 @@ def send_thumb(request, picture_id):
|
||||
|
||||
class AlbumView(CanViewMixin, UseFragmentsMixin, DetailView):
|
||||
model = Album
|
||||
# exclude the SAS from the album accessible with this view
|
||||
# the SAS can be viewed only with SASMainView
|
||||
queryset = Album.objects.exclude(id=settings.SITH_SAS_ROOT_DIR_ID)
|
||||
pk_url_kwarg = "album_id"
|
||||
template_name = "sas/album.jinja"
|
||||
|
||||
@@ -262,6 +267,7 @@ class PictureAskRemovalView(CanViewMixin, DetailView, FormView):
|
||||
|
||||
class AlbumEditView(CanEditMixin, UpdateView):
|
||||
model = Album
|
||||
queryset = Album.objects.exclude(id=settings.SITH_SAS_ROOT_DIR_ID)
|
||||
form_class = AlbumEditForm
|
||||
template_name = "core/edit.jinja"
|
||||
pk_url_kwarg = "album_id"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.12, <4.0"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13'",
|
||||
"python_full_version < '3.13'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alabaster"
|
||||
@@ -61,16 +65,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "backrefs"
|
||||
version = "6.2"
|
||||
version = "7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -131,11 +134,11 @@ redis = [
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.2.25"
|
||||
version = "2026.4.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -279,14 +282,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.2"
|
||||
version = "8.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -439,55 +442,55 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.7"
|
||||
version = "47.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -786,23 +789,23 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "faker"
|
||||
version = "40.13.0"
|
||||
version = "40.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/95/4822ffe94723553789aef783104f4f18fc20d7c4c68e1bbd633e11d09758/faker-40.13.0.tar.gz", hash = "sha256:a0751c84c3abac17327d7bb4c98e8afe70ebf7821e01dd7d0b15cd8856415525", size = 1962043, upload-time = "2026-04-06T16:44:55.68Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7f/13/6741787bd91c4109c7bed047d68273965cd52ce8a5f773c471b949334b6d/faker-40.15.0.tar.gz", hash = "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", size = 1967447, upload-time = "2026-04-17T20:05:27.555Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/8a/708103325edff16a0b0e004de0d37db8ba216a32713948c64d71f6d4a4c2/faker-40.13.0-py3-none-any.whl", hash = "sha256:c1298fd0d819b3688fb5fd358c4ba8f56c7c8c740b411fd3dbd8e30bf2c05019", size = 1994597, upload-time = "2026-04-06T16:44:53.698Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/a7/a600f8f30d4505e89166de51dd121bd540ab8e560e8cf0901de00a81de8c/faker-40.15.0-py3-none-any.whl", hash = "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318", size = 2004447, upload-time = "2026-04-17T20:05:25.437Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.25.2"
|
||||
version = "3.29.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -912,34 +915,54 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ical"
|
||||
version = "11.1.0"
|
||||
version = "12.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "tzdata" },
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.13'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/f5/356c0152b3a5f79bb9b7af77cd1316c18783c886c9ae57c8464bd64ca5b6/ical-11.1.0.tar.gz", hash = "sha256:b8374591ebc769b3e634d3d974b2ccb63ae1fe7751c3121f03b4bc52e83c51ac", size = 124457, upload-time = "2025-10-29T02:03:02.041Z" }
|
||||
dependencies = [
|
||||
{ name = "pydantic", marker = "python_full_version < '3.13'" },
|
||||
{ name = "python-dateutil", marker = "python_full_version < '3.13'" },
|
||||
{ name = "tzdata", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0c/a3/506fe8d9416472588ff30d4c97a7e477f680a098c74a9f2050bd84182a75/ical-12.0.0.tar.gz", hash = "sha256:2f1c7450bd1eff586322273360f44931eb0919dc048c7284f1647037624be7c4", size = 125412, upload-time = "2025-11-13T06:00:04.95Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/63/65bf332cb8b72e33c49a19eed963c1df7bfe8914e9c1bcee1158b38d3e14/ical-11.1.0-py3-none-any.whl", hash = "sha256:e6ce2814a1ec08cc522b128e1e8b34571acc7790a1e85e14d3d203e31a098d4b", size = 122858, upload-time = "2025-10-29T02:03:00.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/5c/628c2bdd3f7194b090eb8bfb7510c1503076f9c5c42143e2ef58d4aa2b85/ical-12.0.0-py3-none-any.whl", hash = "sha256:677f82d098d627d234785345aefe5f567bbf65e1ece31ed4ca3294e3cecc56fe", size = 123718, upload-time = "2025-11-13T06:00:03.08Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ical"
|
||||
version = "13.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.13'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "pydantic", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "python-dateutil", marker = "python_full_version >= '3.13'" },
|
||||
{ name = "tzdata", marker = "python_full_version >= '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/da/810acf4d830cfe6a7fc05da04d993106c832dc70d3de81567706d704e877/ical-13.2.2.tar.gz", hash = "sha256:160e4f33903d2a5b4e4c4304b2bf31f4b9050f86ff7c55dc937a998ac021f271", size = 130219, upload-time = "2026-03-16T04:22:45.204Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/7c/50e9a34bc42dc0523b833cd0de090c1aa1ee98e9b40c327542cbf51c53f5/ical-13.2.2-py3-none-any.whl", hash = "sha256:a00ba88d9154cd9bf3609249f3bef65a90eb462c19137e3e546fdd237b89ea2d", size = 128235, upload-time = "2026-03-16T04:22:42.979Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.18"
|
||||
version = "2.6.19"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/c4/7fb4db12296cdb11893d61c92048fe617ee853f8523b9b296ac03b43757e/identify-2.6.18.tar.gz", hash = "sha256:873ac56a5e3fd63e7438a7ecbc4d91aca692eb3fefa4534db2b7913f3fc352fd", size = 99580, upload-time = "2026-03-15T18:39:50.319Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/46/33/92ef41c6fad0233e41d3d84ba8e8ad18d1780f1e5d99b3c683e6d7f98b63/identify-2.6.18-py2.py3-none-any.whl", hash = "sha256:8db9d3c8ea9079db92cafb0ebf97abdc09d52e97f4dcf773a2e694048b7cd737", size = 99394, upload-time = "2026-03-15T18:39:48.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
version = "3.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -971,7 +994,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ipython"
|
||||
version = "9.12.0"
|
||||
version = "9.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
@@ -981,13 +1004,14 @@ dependencies = [
|
||||
{ name = "matplotlib-inline" },
|
||||
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
{ name = "prompt-toolkit" },
|
||||
{ name = "psutil" },
|
||||
{ name = "pygments" },
|
||||
{ name = "stack-data" },
|
||||
{ name = "traitlets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3a/73/7114f80a8f9cabdb13c27732dce24af945b2923dcab80723602f7c8bc2d8/ipython-9.12.0.tar.gz", hash = "sha256:01daa83f504b693ba523b5a407246cabde4eb4513285a3c6acaff11a66735ee4", size = 4428879, upload-time = "2026-03-27T09:42:45.312Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/87cda5842cf5c31837c06ddb588e11c3c35d8ece89b7a0108c06b8c9b00a/ipython-9.13.0.tar.gz", hash = "sha256:7e834b6afc99f020e3f05966ced34792f40267d64cb1ea9043886dab0dde5967", size = 4430549, upload-time = "2026-04-24T12:24:55.221Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/59/22/906c8108974c673ebef6356c506cebb6870d48cedea3c41e949e2dd556bb/ipython-9.12.0-py3-none-any.whl", hash = "sha256:0f2701e8ee86e117e37f50563205d36feaa259d2e08d4a6bc6b6d74b18ce128d", size = 625661, upload-time = "2026-03-27T09:42:42.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/86/3060e8029b7cc505cce9a0137431dda81d0a3fde93a8f0f50ee0bf37a795/ipython-9.13.0-py3-none-any.whl", hash = "sha256:57f9d4639e20818d328d287c7b549af3d05f12486ea8f2e7f73e52a36ec4d201", size = 627274, upload-time = "2026-04-24T12:24:53.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1061,82 +1085,82 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "6.0.2"
|
||||
version = "6.1.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/28/30/9abc9e34c657c33834eaf6cd02124c61bdf5944d802aa48e69be8da3585d/lxml-6.1.0.tar.gz", hash = "sha256:bfd57d8008c4965709a919c3e9a98f76c2c7cb319086b3d26858250620023b13", size = 4197006, upload-time = "2026-04-18T04:32:51.613Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/d4/9326838b59dc36dfae42eec9656b97520f9997eee1de47b8316aaeed169c/lxml-6.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d2f17a16cd8751e8eb233a7e41aecdf8e511712e00088bf9be455f604cd0d28d", size = 8570663, upload-time = "2026-04-18T04:27:48.253Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/a4/053745ce1f8303ccbb788b86c0db3a91b973675cefc42566a188637b7c40/lxml-6.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0cea5b1d3e6e77d71bd2b9972eb2446221a69dc52bb0b9c3c6f6e5700592d93", size = 4624024, upload-time = "2026-04-18T04:27:52.594Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/97/a517944b20f8fd0932ad2109482bee4e29fe721416387a363306667941f6/lxml-6.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fc46da94826188ed45cb53bd8e3fc076ae22675aea2087843d4735627f867c6d", size = 4930895, upload-time = "2026-04-18T04:32:56.29Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/7c/e08a970727d556caa040a44773c7b7e3ad0f0d73dedc863543e9a8b931f2/lxml-6.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9147d8e386ec3b82c3b15d88927f734f565b0aaadef7def562b853adca45784a", size = 5093820, upload-time = "2026-04-18T04:32:58.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ee/2a5c2aa2c32016a226ca25d3e1056a8102ea6e1fe308bf50213586635400/lxml-6.1.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5715e0e28736a070f3f34a7ccc09e2fdcba0e3060abbcf61a1a5718ff6d6b105", size = 5005790, upload-time = "2026-04-18T04:33:01.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/38/a0db9be8f38ad6043ab9429487c128dd1d30f07956ef43040402f8da49e8/lxml-6.1.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4937460dc5df0cdd2f06a86c285c28afda06aefa3af949f9477d3e8df430c485", size = 5630827, upload-time = "2026-04-18T04:33:04.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/ba/3c13d3fc24b7cacf675f808a3a1baabf43a30d0cd24c98f94548e9aa58eb/lxml-6.1.0-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bc783ee3147e60a25aa0445ea82b3e8aabb83b240f2b95d32cb75587ff781814", size = 5240445, upload-time = "2026-04-18T04:33:06.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/ba/eeef4ccba09b2212fe239f46c1692a98db1878e0872ae320756488878a94/lxml-6.1.0-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:40d9189f80075f2e1f88db21ef815a2b17b28adf8e50aaf5c789bfe737027f32", size = 5350121, upload-time = "2026-04-18T04:33:09.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/01/1da87c7b587c38d0cbe77a01aae3b9c1c49ed47d76918ef3db8fc151b1ca/lxml-6.1.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:05b9b8787e35bec69e68daf4952b2e6dfcfb0db7ecf1a06f8cdfbbac4eb71aad", size = 4694949, upload-time = "2026-04-18T04:33:11.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/88/7db0fe66d5aaf128443ee1623dec3db1576f3e4c17751ec0ef5866468590/lxml-6.1.0-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0f08beb0182e3e9a86fae124b3c47a7b41b7b69b225e1377db983802404e54", size = 5243901, upload-time = "2026-04-18T04:33:13.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/a8/1346726af7d1f6fca1f11223ba34001462b0a3660416986d37641708d57c/lxml-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73becf6d8c81d4c76b1014dbd3584cb26d904492dcf73ca85dc8bff08dcd6d2d", size = 5048054, upload-time = "2026-04-18T04:33:16.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/b7/85057012f035d1a0c87e02f8c723ca3c3e6e0728bcf4cb62080b21b1c1e3/lxml-6.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1ae225f66e5938f4fa29d37e009a3bb3b13032ac57eb4eb42afa44f6e4054e69", size = 4777324, upload-time = "2026-04-18T04:33:19.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/6c/ad2f94a91073ef570f33718040e8e160d5fb93331cf1ab3ca1323f939e2d/lxml-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:690022c7fae793b0489aa68a658822cea83e0d5933781811cabbf5ea3bcfe73d", size = 5645702, upload-time = "2026-04-18T04:33:22.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/89/0bb6c0bd549c19004c60eea9dc554dd78fd647b72314ef25d460e0d208c6/lxml-6.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:63aeafc26aac0be8aff14af7871249e87ea1319be92090bfd632ec68e03b16a5", size = 5232901, upload-time = "2026-04-18T04:33:26.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/d9/d609a11fb567da9399f525193e2b49847b5a409cdebe737f06a8b7126bdc/lxml-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:264c605ab9c0e4aa1a679636f4582c4d3313700009fac3ec9c3412ed0d8f3e1d", size = 5261333, upload-time = "2026-04-18T04:33:28.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/3a/ac3f99ec8ac93089e7dd556f279e0d14c24de0a74a507e143a2e4b496e7c/lxml-6.1.0-cp312-cp312-win32.whl", hash = "sha256:56971379bc5ee8037c5a0f09fa88f66cdb7d37c3e38af3e45cf539f41131ac1f", size = 3596289, upload-time = "2026-04-18T04:27:42.819Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/a7/0a915557538593cb1bbeedcd40e13c7a261822c26fecbbdb71dad0c2f540/lxml-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:bba078de0031c219e5dd06cf3e6bf8fb8e6e64a77819b358f53bb132e3e03366", size = 3997059, upload-time = "2026-04-18T04:27:46.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/96/a5dc078cf0126fbfbc35611d77ecd5da80054b5893e28fb213a5613b9e1d/lxml-6.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:c3592631e652afa34999a088f98ba7dfc7d6aff0d535c410bea77a71743f3819", size = 3659552, upload-time = "2026-04-18T04:27:51.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/03/69347590f1cf4a6d5a4944bb6099e6d37f334784f16062234e1f892fdb1d/lxml-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a0092f2b107b69601adf562a57c956fbb596e05e3e6651cabd3054113b007e45", size = 8559689, upload-time = "2026-04-18T04:31:57.785Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/58/25e00bb40b185c974cfe156c110474d9a8a8390d5f7c92a4e328189bb60e/lxml-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc7140d7a7386e6b545d41b7358f4d02b656d4053f5fa6859f92f4b9c2572c4d", size = 4617892, upload-time = "2026-04-18T04:32:01.78Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/54/92ad98a94ac318dc4f97aaac22ff8d1b94212b2ae8af5b6e9b354bf825f7/lxml-6.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:419c58fc92cc3a2c3fa5f78c63dbf5da70c1fa9c1b25f25727ecee89a96c7de2", size = 4923489, upload-time = "2026-04-18T04:33:31.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/3b/a20aecfab42bdf4f9b390590d345857ad3ffd7c51988d1c89c53a0c73faf/lxml-6.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:37fabd1452852636cf38ecdcc9dd5ca4bba7a35d6c53fa09725deeb894a87491", size = 5082162, upload-time = "2026-04-18T04:33:34.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/26/2cdb3d281ac1bd175603e290cbe4bad6eff127c0f8de90bafd6f8548f0fd/lxml-6.1.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2853c8b2170cc6cd54a6b4d50d2c1a8a7aeca201f23804b4898525c7a152cfc", size = 4993247, upload-time = "2026-04-18T04:33:36.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/05/d735aef963740022a08185c84821f689fc903acb3d50326e6b1e9886cc22/lxml-6.1.0-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e369cbd690e788c8d15e56222d91a09c6a417f49cbc543040cba0fe2e25a79e", size = 5613042, upload-time = "2026-04-18T04:33:39.205Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/b8/ead7c10efff731738c72e59ed6eb5791854879fbed7ae98781a12006263a/lxml-6.1.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e69aa6805905807186eb00e66c6d97a935c928275182eb02ee40ba00da9623b2", size = 5228304, upload-time = "2026-04-18T04:33:41.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/10/e9842d2ec322ea65f0a7270aa0315a53abed06058b88ef1b027f620e7a5f/lxml-6.1.0-cp313-cp313-manylinux_2_28_i686.whl", hash = "sha256:4bd1bdb8a9e0e2dd229de19b5f8aebac80e916921b4b2c6ef8a52bc131d0c1f9", size = 5341578, upload-time = "2026-04-18T04:33:44.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/54/40d9403d7c2775fa7301d3ddd3464689bfe9ba71acc17dfff777071b4fdc/lxml-6.1.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:cbd7b79cdcb4986ad78a2662625882747f09db5e4cd7b2ae178a88c9c51b3dfe", size = 4700209, upload-time = "2026-04-18T04:33:47.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/b2/bbdcc2cf45dfc7dfffef4fd97e5c47b15919b6a365247d95d6f684ef5e82/lxml-6.1.0-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:43e4d297f11080ec9d64a4b1ad7ac02b4484c9f0e2179d9c4ef78e886e747b88", size = 5232365, upload-time = "2026-04-18T04:33:50.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5a/b06875665e53aaba7127611a7bed3b7b9658e20b22bc2dd217a0b7ab0091/lxml-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cc16682cc987a3da00aa56a3aa3075b08edb10d9b1e476938cfdbee8f3b67181", size = 5043654, upload-time = "2026-04-18T04:33:52.71Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/9c/e71a069d09641c1a7abeb30e693f828c7c90a41cbe3d650b2d734d876f85/lxml-6.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d8efe71429635f0559579092bb5e60560d7b9115ee38c4adbea35632e7fa24", size = 4769326, upload-time = "2026-04-18T04:33:55.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/06/7a9cd84b3d4ed79adf35f874750abb697dec0b4a81a836037b36e47c091a/lxml-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e39ab3a28af7784e206d8606ec0e4bcad0190f63a492bca95e94e5a4aef7f6e", size = 5635879, upload-time = "2026-04-18T04:33:58.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/f0/9d57916befc1e54c451712c7ee48e9e74e80ae4d03bdce49914e0aee42cd/lxml-6.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9eb667bf50856c4a58145f8ca2d5e5be160191e79eb9e30855a476191b3c3495", size = 5224048, upload-time = "2026-04-18T04:34:00.943Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/75/90c4eefda0c08c92221fe0753db2d6699a4c628f76ff4465ec20dea84cc1/lxml-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7f4a77d6f7edf9230cee3e1f7f6764722a41604ee5681844f18db9a81ea0ec33", size = 5250241, upload-time = "2026-04-18T04:34:03.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/73/16596f7e4e38fa33084b9ccbccc22a15f82a290a055126f2c1541236d2ff/lxml-6.1.0-cp313-cp313-win32.whl", hash = "sha256:28902146ffbe5222df411c5d19e5352490122e14447e98cd118907ee3fd6ee62", size = 3596938, upload-time = "2026-04-18T04:31:56.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/63/981401c5680c1eb30893f00a19641ac80db5d1e7086c62cb4b13ed813038/lxml-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:4a1503c56e4e2b38dc76f2f2da7bae69670c0f1933e27cfa34b2fa5876410b16", size = 3995728, upload-time = "2026-04-18T04:31:58.763Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/e8/c358a38ac3e541d16a1b527e4e9cb78c0419b0506a070ace11777e5e8404/lxml-6.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:e0af85773850417d994d019741239b901b22c6680206f46a34766926e466141d", size = 3658372, upload-time = "2026-04-18T04:32:03.629Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/45/cee4cf203ef0bab5c52afc118da61d6b460c928f2893d40023cfa27e0b80/lxml-6.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ab863fd37458fed6456525f297d21239d987800c46e67da5ef04fc6b3dd93ac8", size = 8576713, upload-time = "2026-04-18T04:32:06.831Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/a7/eda05babeb7e046839204eaf254cd4d7c9130ce2bbf0d9e90ea41af5654d/lxml-6.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6fd8b1df8254ff4fd93fd31da1fc15770bde23ac045be9bb1f87425702f61cc9", size = 4623874, upload-time = "2026-04-18T04:32:10.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/e9/db5846de9b436b91890a62f29d80cd849ea17948a49bf532d5278ee69a9e/lxml-6.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:47024feaae386a92a146af0d2aeed65229bf6fff738e6a11dda6b0015fb8fd03", size = 4949535, upload-time = "2026-04-18T04:34:06.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/ba/0d3593373dcae1d68f40dc3c41a5a92f2544e68115eb2f62319a4c2a6500/lxml-6.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3f00972f84450204cd5d93a5395965e348956aaceaadec693a22ec743f8ae3eb", size = 5086881, upload-time = "2026-04-18T04:34:09.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/76/759a7484539ad1af0d125a9afe9c3fb5f82a8779fd1f5f56319d9e4ea2fd/lxml-6.1.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97faa0860e13b05b15a51fb4986421ef7a30f0b3334061c416e0981e9450ca4c", size = 5031305, upload-time = "2026-04-18T04:34:12.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/b9/c1f0daf981a11e47636126901fd4ab82429e18c57aeb0fc3ad2940b42d8b/lxml-6.1.0-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:972a6451204798675407beaad97b868d0c733d9a74dafefc63120b81b8c2de28", size = 5647522, upload-time = "2026-04-18T04:34:14.89Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/e6/1f533dcd205275363d9ba3511bcec52fa2df86abf8abe6a5f2c599f0dc31/lxml-6.1.0-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe022f20bc4569ec66b63b3fb275a3d628d9d32da6326b2982584104db6d3086", size = 5239310, upload-time = "2026-04-18T04:34:17.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/8c/4175fb709c78a6e315ed814ed33be3defd8b8721067e70419a6cf6f971da/lxml-6.1.0-cp314-cp314-manylinux_2_28_i686.whl", hash = "sha256:75c4c7c619a744f972f4451bf5adf6d0fb00992a1ffc9fd78e13b0bc817cc99f", size = 5350799, upload-time = "2026-04-18T04:34:20.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/77/6ffdebc5994975f0dde4acb59761902bd9d9bb84422b9a0bd239a7da9ca8/lxml-6.1.0-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:3648f20d25102a22b6061c688beb3a805099ea4beb0a01ce62975d926944d292", size = 4697693, upload-time = "2026-04-18T04:34:23.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/f1/565f36bd5c73294602d48e04d23f81ff4c8736be6ba5e1d1ec670ac9be80/lxml-6.1.0-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:77b9f99b17cbf14026d1e618035077060fc7195dd940d025149f3e2e830fbfcb", size = 5250708, upload-time = "2026-04-18T04:34:26.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/11/a68ab9dd18c5c499404deb4005f4bc4e0e88e5b72cd755ad96efec81d18d/lxml-6.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32662519149fd7a9db354175aa5e417d83485a8039b8aaa62f873ceee7ea4cad", size = 5084737, upload-time = "2026-04-18T04:34:28.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/78/e8f41e2c74f4af564e6a0348aea69fb6daaefa64bc071ef469823d22cc18/lxml-6.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:73d658216fc173cf2c939e90e07b941c5e12736b0bf6a99e7af95459cfe8eabb", size = 4737817, upload-time = "2026-04-18T04:34:30.784Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/2d/aa4e117aa2ce2f3b35d9ff246be74a2f8e853baba5d2a92c64744474603a/lxml-6.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ac4db068889f8772a4a698c5980ec302771bb545e10c4b095d4c8be26749616f", size = 5670753, upload-time = "2026-04-18T04:34:33.675Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/f5/dd745d50c0409031dbfcc4881740542a01e54d6f0110bd420fa7782110b8/lxml-6.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:45e9dfbd1b661eb64ba0d4dbe762bd210c42d86dd1e5bd2bdf89d634231beb43", size = 5238071, upload-time = "2026-04-18T04:34:36.12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/74/ad424f36d0340a904665867dab310a3f1f4c96ff4039698de83b77f44c1f/lxml-6.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:89e8d73d09ac696a5ba42ec69787913d53284f12092f651506779314f10ba585", size = 5264319, upload-time = "2026-04-18T04:34:39.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/36/a15d8b3514ec889bfd6aa3609107fcb6c9189f8dc347f1c0b81eded8d87c/lxml-6.1.0-cp314-cp314-win32.whl", hash = "sha256:ebe33f4ec1b2de38ceb225a1749a2965855bffeef435ba93cd2d5d540783bf2f", size = 3657139, upload-time = "2026-04-18T04:32:20.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/a4/263ebb0710851a3c6c937180a9a86df1206fdfe53cc43005aa2237fd7736/lxml-6.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:398443df51c538bd578529aa7e5f7afc6c292644174b47961f3bf87fe5741120", size = 4064195, upload-time = "2026-04-18T04:32:23.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/68/2000f29d323b6c286de077ad20b429fc52272e44eae6d295467043e56012/lxml-6.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:8c8984e1d8c4b3949e419158fda14d921ff703a9ed8a47236c6eb7a2b6cb4946", size = 3741870, upload-time = "2026-04-18T04:32:27.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/e9/21383c7c8d43799f0da90224c0d7c921870d476ec9b3e01e1b2c0b8237c5/lxml-6.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1081dd10bc6fa437db2500e13993abf7cc30716d0a2f40e65abb935f02ec559c", size = 8827548, upload-time = "2026-04-18T04:32:15.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/01/c6bc11cd587030dd4f719f65c5657960649fe3e19196c844c75bf32cd0d6/lxml-6.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dabecc48db5f42ba348d1f5d5afdc54c6c4cc758e676926c7cd327045749517d", size = 4735866, upload-time = "2026-04-18T04:32:18.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/01/757132fff5f4acf25463b5298f1a46099f3a94480b806547b29ce5e385de/lxml-6.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e3dd5fe19c9e0ac818a9c7f132a5e43c1339ec1cbbfecb1a938bd3a47875b7c9", size = 4969476, upload-time = "2026-04-18T04:34:41.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/fb/1bc8b9d27ed64be7c8903db6c89e74dc8c2cd9ec630a7462e4654316dc5b/lxml-6.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9e7b0a4ca6dcc007a4cef00a761bba2dea959de4bd2df98f926b33c92ca5dfb9", size = 5103719, upload-time = "2026-04-18T04:34:44.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/e7/5bf82fa28133536a54601aae633b14988e89ed61d4c1eb6b899b023233aa/lxml-6.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d27bbe326c6b539c64b42638b18bc6003a8d88f76213a97ac9ed4f885efeab7", size = 5027890, upload-time = "2026-04-18T04:34:47.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/20/e048db5d4b4ea0366648aa595f26bb764b2670903fc585b87436d0a5032c/lxml-6.1.0-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4e425db0c5445ef0ad56b0eec54f89b88b2d884656e536a90b2f52aecb4ca86", size = 5596008, upload-time = "2026-04-18T04:34:51.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/c2/d10807bc8da4824b39e5bd01b5d05c077b6fd01bd91584167edf6b269d22/lxml-6.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b89b098105b8599dc57adac95d1813409ac476d3c948a498775d3d0c6124bfb", size = 5224451, upload-time = "2026-04-18T04:34:54.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/15/2ebea45bea427e7f0057e9ce7b2d62c5aba20c6b001cca89ed0aadb3ad41/lxml-6.1.0-cp314-cp314t-manylinux_2_28_i686.whl", hash = "sha256:c4a699432846df86cc3de502ee85f445ebad748a1c6021d445f3e514d2cd4b1c", size = 5312135, upload-time = "2026-04-18T04:34:56.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/e2/87eeae151b0be2a308d49a7ec444ff3eb192b14251e62addb29d0bf3778f/lxml-6.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:30e7b2ed63b6c8e97cca8af048589a788ab5c9c905f36d9cf1c2bb549f450d2f", size = 4639126, upload-time = "2026-04-18T04:34:59.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/51/8a3f6a20902ad604dd746ec7b4000311b240d389dac5e9d95adefd349e0c/lxml-6.1.0-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:022981127642fe19866d2907d76241bb07ed21749601f727d5d5dd1ce5d1b773", size = 5232579, upload-time = "2026-04-18T04:35:02.658Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/d2/650d619bdbe048d2c3f2c31edb00e35670a5e2d65b4fe3b61bce37b19121/lxml-6.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:23cad0cc86046d4222f7f418910e46b89971c5a45d3c8abfad0f64b7b05e4a9b", size = 5084206, upload-time = "2026-04-18T04:35:05.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/8a/672ca1a3cbeabd1f511ca275a916c0514b747f4b85bdaae103b8fa92f307/lxml-6.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:21c3302068f50d1e8728c67c87ba92aa87043abee517aa2576cca1855326b405", size = 4758906, upload-time = "2026-04-18T04:35:08.098Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/f1/ef4b691da85c916cb2feb1eec7414f678162798ac85e042fa164419ac05c/lxml-6.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:be10838781cb3be19251e276910cd508fe127e27c3242e50521521a0f3781690", size = 5620553, upload-time = "2026-04-18T04:35:11.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/17/94e81def74107809755ac2782fdad4404420f1c92ca83433d117a6d5acf0/lxml-6.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2173a7bffe97667bbf0767f8a99e587740a8c56fdf3befac4b09cb29a80276fd", size = 5229458, upload-time = "2026-04-18T04:35:14.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/55/c4be91b0f830a871fc1b0d730943d56013b683d4671d5198260e2eae722b/lxml-6.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c6854e9cf99c84beb004eecd7d3a3868ef1109bf2b1df92d7bc11e96a36c2180", size = 5247861, upload-time = "2026-04-18T04:35:17.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/ca/77123e4d77df3cb1e968ade7b1f808f5d3a5c1c96b18a33895397de292c1/lxml-6.1.0-cp314-cp314t-win32.whl", hash = "sha256:00750d63ef0031a05331b9223463b1c7c02b9004cef2346a5b2877f0f9494dd2", size = 3897377, upload-time = "2026-04-18T04:32:07.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/ce/3554833989d074267c063209bae8b09815e5656456a2d332b947806b05ff/lxml-6.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:80410c3a7e3c617af04de17caa9f9f20adaa817093293d69eae7d7d0522836f5", size = 4392701, upload-time = "2026-04-18T04:32:12.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/a0/9b916c68c0e57752c07f8f64b30138d9d4059dbeb27b90274dedbea128ff/lxml-6.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:26dd9f57ee3bd41e7d35b4c98a2ffd89ed11591649f421f0ec19f67d50ec67ac", size = 3817120, upload-time = "2026-04-18T04:32:15.803Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1348,7 +1372,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jinja2" },
|
||||
@@ -1358,9 +1382,9 @@ dependencies = [
|
||||
{ name = "mkdocs-autorefs" },
|
||||
{ name = "pymdown-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1400,11 +1424,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1427,11 +1451,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "1.0.4"
|
||||
version = "1.1.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1448,11 +1472,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "phonenumbers"
|
||||
version = "9.0.27"
|
||||
version = "9.0.29"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e3/d7/8e743354071ab0d6e265370b47a8b0ecc70f35f4e225eb74eecab141aa9f/phonenumbers-9.0.27.tar.gz", hash = "sha256:f99f533cfb052546d181f6dbd981bfbdd6a1527784585482173ff95ae6d1602e", size = 2298619, upload-time = "2026-04-01T11:13:15.535Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/c3/5b544d133241d649317c0ef456df4705cddb3968d31b5f64ec34e98925aa/phonenumbers-9.0.29.tar.gz", hash = "sha256:66455e5c5ca4197aca5a4217d0dc82717a22038292cfa322d5d5e7c46934a214", size = 2300063, upload-time = "2026-04-25T05:35:37.574Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/40/c6948bbeceb18c45472d4011b705bd3ca4f29b055373eb4733ddc1a85d8b/phonenumbers-9.0.27-py2.py3-none-any.whl", hash = "sha256:a7ba9f362e2fe8e2d6756e415cf44f4c8f0c33b40ba07b27a8450909021d7e37", size = 2585047, upload-time = "2026-04-01T11:13:12.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/65/d932a84540a2d188c5d0dc3a80d7063820588bbc310cc59b4fe48619be9e/phonenumbers-9.0.29-py2.py3-none-any.whl", hash = "sha256:0eb26e0c578aa2d56428330d5b4a0a07dac9a452c4387bf76a76a88b1e839ab7", size = 2586805, upload-time = "2026-04-25T05:35:34.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1526,11 +1550,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.9.4"
|
||||
version = "4.9.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1544,7 +1568,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.5.1"
|
||||
version = "4.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cfgv" },
|
||||
@@ -1553,9 +1577,9 @@ dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "virtualenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1651,7 +1675,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.12.5"
|
||||
version = "2.13.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
@@ -1659,93 +1683,97 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.41.5"
|
||||
version = "2.46.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/cb/5b47425556ecc1f3fe18ed2a0083188aa46e1dd812b06e406475b3a5d536/pydantic_core-2.46.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b11b59b3eee90a80a36701ddb4576d9ae31f93f05cb9e277ceaa09e6bf074a67", size = 2101946, upload-time = "2026-04-20T14:40:52.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/4f/2fb62c2267cae99b815bbf4a7b9283812c88ca3153ef29f7707200f1d4e5/pydantic_core-2.46.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af8653713055ea18a3abc1537fe2ebc42f5b0bbb768d1eb79fd74eb47c0ac089", size = 1951612, upload-time = "2026-04-20T14:42:42.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/6e/b7348fd30d6556d132cddd5bd79f37f96f2601fe0608afac4f5fb01ec0b3/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75a519dab6d63c514f3a81053e5266c549679e4aa88f6ec57f2b7b854aceb1b0", size = 1977027, upload-time = "2026-04-20T14:42:02.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/11/31d60ee2b45540d3fb0b29302a393dbc01cd771c473f5b5147bcd353e593/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6cd87cb1575b1ad05ba98894c5b5c96411ef678fa2f6ed2576607095b8d9789", size = 2063008, upload-time = "2026-04-20T14:44:17.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/db/3a9d1957181b59258f44a2300ab0f0be9d1e12d662a4f57bb31250455c52/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f80a55484b8d843c8ada81ebf70a682f3f00a3d40e378c06cf17ecb44d280d7d", size = 2233082, upload-time = "2026-04-20T14:40:57.934Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/e1/3277c38792aeb5cfb18c2f0c5785a221d9ff4e149abbe1184d53d5f72273/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3861f1731b90c50a3266316b9044f5c9b405eecb8e299b0a7120596334e4fe9c", size = 2304615, upload-time = "2026-04-20T14:42:12.584Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/d5/e3d9717c9eba10855325650afd2a9cba8e607321697f18953af9d562da2f/pydantic_core-2.46.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb528e295ed31570ac3dcc9bfdd6e0150bc11ce6168ac87a8082055cf1a67395", size = 2094380, upload-time = "2026-04-20T14:43:05.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/20/abac35dedcbfd66c6f0b03e4e3564511771d6c9b7ede10a362d03e110d9b/pydantic_core-2.46.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:367508faa4973b992b271ba1494acaab36eb7e8739d1e47be5035fb1ea225396", size = 2135429, upload-time = "2026-04-20T14:41:55.549Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/a5/41bfd1df69afad71b5cf0535055bccc73022715ad362edbc124bc1e021d7/pydantic_core-2.46.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ad3c826fe523e4becf4fe39baa44286cff85ef137c729a2c5e269afbfd0905d", size = 2174582, upload-time = "2026-04-20T14:41:45.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/65/38d86ea056b29b2b10734eb23329b7a7672ca604df4f2b6e9c02d4ee22fe/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ec638c5d194ef8af27db69f16c954a09797c0dc25015ad6123eb2c73a4d271ca", size = 2187533, upload-time = "2026-04-20T14:40:55.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/55/a1129141678a2026badc539ad1dee0a71d06f54c2f06a4bd68c030ac781b/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:28ed528c45446062ee66edb1d33df5d88828ae167de76e773a3c7f64bd14e976", size = 2332985, upload-time = "2026-04-20T14:44:13.05Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/60/cb26f4077719f709e54819f4e8e1d43f4091f94e285eb6bd21e1190a7b7c/pydantic_core-2.46.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aed19d0c783886d5bd86d80ae5030006b45e28464218747dcf83dabfdd092c7b", size = 2373670, upload-time = "2026-04-20T14:41:53.421Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/7e/c3f21882bdf1d8d086876f81b5e296206c69c6082551d776895de7801fa0/pydantic_core-2.46.3-cp312-cp312-win32.whl", hash = "sha256:06d5d8820cbbdb4147578c1fe7ffcd5b83f34508cb9f9ab76e807be7db6ff0a4", size = 1966722, upload-time = "2026-04-20T14:44:30.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/be/6b5e757b859013ebfbd7adba02f23b428f37c86dcbf78b5bb0b4ffd36e99/pydantic_core-2.46.3-cp312-cp312-win_amd64.whl", hash = "sha256:c3212fda0ee959c1dd04c60b601ec31097aaa893573a3a1abd0a47bcac2968c1", size = 2072970, upload-time = "2026-04-20T14:42:54.248Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/f8/a989b21cc75e9a32d24192ef700eea606521221a89faa40c919ce884f2b1/pydantic_core-2.46.3-cp312-cp312-win_arm64.whl", hash = "sha256:f1f8338dd7a7f31761f1f1a3c47503a9a3b34eea3c8b01fa6ee96408affb5e72", size = 2035963, upload-time = "2026-04-20T14:44:20.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/3c/9b5e8eb9821936d065439c3b0fb1490ffa64163bfe7e1595985a47896073/pydantic_core-2.46.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:12bc98de041458b80c86c56b24df1d23832f3e166cbaff011f25d187f5c62c37", size = 2102109, upload-time = "2026-04-20T14:41:24.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/97/1c41d1f5a19f241d8069f1e249853bcce378cdb76eec8ab636d7bc426280/pydantic_core-2.46.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:85348b8f89d2c3508b65b16c3c33a4da22b8215138d8b996912bb1532868885f", size = 1951820, upload-time = "2026-04-20T14:42:14.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/b4/d03a7ae14571bc2b6b3c7b122441154720619afe9a336fa3a95434df5e2f/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1105677a6df914b1fb71a81b96c8cce7726857e1717d86001f29be06a25ee6f8", size = 1977785, upload-time = "2026-04-20T14:42:31.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/0c/4086f808834b59e3c8f1aa26df8f4b6d998cdcf354a143d18ef41529d1fe/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87082cd65669a33adeba5470769e9704c7cf026cc30afb9cc77fd865578ebaad", size = 2062761, upload-time = "2026-04-20T14:40:37.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/71/a649be5a5064c2df0db06e0a512c2281134ed2fcc981f52a657936a7527c/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e5f66e12c4f5212d08522963380eaaeac5ebd795826cfd19b2dfb0c7a52b9c", size = 2232989, upload-time = "2026-04-20T14:42:59.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/84/7756e75763e810b3a710f4724441d1ecc5883b94aacb07ca71c5fb5cfb69/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6cdf19bf84128d5e7c37e8a73a0c5c10d51103a650ac585d42dd6ae233f2b7f", size = 2303975, upload-time = "2026-04-20T14:41:32.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/35/68a762e0c1e31f35fa0dac733cbd9f5b118042853698de9509c8e5bf128b/pydantic_core-2.46.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031bb17f4885a43773c8c763089499f242aee2ea85cf17154168775dccdecf35", size = 2095325, upload-time = "2026-04-20T14:42:47.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/bf/1bf8c9a8e91836c926eae5e3e51dce009bf495a60ca56060689d3df3f340/pydantic_core-2.46.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:bcf2a8b2982a6673693eae7348ef3d8cf3979c1d63b54fca7c397a635cc68687", size = 2133368, upload-time = "2026-04-20T14:41:22.766Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/50/87d818d6bab915984995157ceb2380f5aac4e563dddbed6b56f0ed057aba/pydantic_core-2.46.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28e8cf2f52d72ced402a137145923a762cbb5081e48b34312f7a0c8f55928ec3", size = 2173908, upload-time = "2026-04-20T14:42:52.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/88/a311fb306d0bd6185db41fa14ae888fb81d0baf648a761ae760d30819d33/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:17eaface65d9fc5abb940003020309c1bf7a211f5f608d7870297c367e6f9022", size = 2186422, upload-time = "2026-04-20T14:43:29.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/79/28fd0d81508525ab2054fef7c77a638c8b5b0afcbbaeee493cf7c3fef7e1/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:93fd339f23408a07e98950a89644f92c54d8729719a40b30c0a30bb9ebc55d23", size = 2332709, upload-time = "2026-04-20T14:42:16.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/21/795bf5fe5c0f379308b8ef19c50dedab2e7711dbc8d0c2acf08f1c7daa05/pydantic_core-2.46.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:23cbdb3aaa74dfe0837975dbf69b469753bbde8eacace524519ffdb6b6e89eb7", size = 2372428, upload-time = "2026-04-20T14:41:10.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/b3/ed14c659cbe7605e3ef063077680a64680aec81eb1a04763a05190d49b7f/pydantic_core-2.46.3-cp313-cp313-win32.whl", hash = "sha256:610eda2e3838f401105e6326ca304f5da1e15393ae25dacae5c5c63f2c275b13", size = 1965601, upload-time = "2026-04-20T14:41:42.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/bb/adb70d9a762ddd002d723fbf1bd492244d37da41e3af7b74ad212609027e/pydantic_core-2.46.3-cp313-cp313-win_amd64.whl", hash = "sha256:68cc7866ed863db34351294187f9b729964c371ba33e31c26f478471c52e1ed0", size = 2071517, upload-time = "2026-04-20T14:43:36.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/eb/66faefabebfe68bd7788339c9c9127231e680b11906368c67ce112fdb47f/pydantic_core-2.46.3-cp313-cp313-win_arm64.whl", hash = "sha256:f64b5537ac62b231572879cd08ec05600308636a5d63bcbdb15063a466977bec", size = 2035802, upload-time = "2026-04-20T14:43:38.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/42/f426db557e8ab2791bc7562052299944a118655496fbff99914e564c0a94/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b12dd51f1187c2eb489af8e20f880362db98e954b54ab792fa5d92e8bcc6b803", size = 2091877, upload-time = "2026-04-20T14:43:27.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/4f/86a832a9d14df58e663bfdf4627dc00d3317c2bd583c4fb23390b0f04b8e/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f00a0961b125f1a47af7bcc17f00782e12f4cd056f83416006b30111d941dfa3", size = 1932428, upload-time = "2026-04-20T14:40:45.781Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/1a/fe857968954d93fb78e0d4b6df5c988c74c4aaa67181c60be7cfe327c0ca/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57697d7c056aca4bbb680200f96563e841a6386ac1129370a0102592f4dddff5", size = 1997550, upload-time = "2026-04-20T14:44:02.425Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/eb/9d89ad2d9b0ba8cd65393d434471621b98912abb10fbe1df08e480ba57b5/pydantic_core-2.46.3-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd35aa21299def8db7ef4fe5c4ff862941a9a158ca7b63d61e66fe67d30416b4", size = 2137657, upload-time = "2026-04-20T14:42:45.149Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-extra-types"
|
||||
version = "2.11.2"
|
||||
version = "2.11.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/d3/3be31542180c0300b6860129ff1e3a428f3ef580727616ce22462626129b/pydantic_extra_types-2.11.2.tar.gz", hash = "sha256:3a2b83b61fe920925688e7838b59caa90a45637d1dbba2b1364b8d1f7ff72a0a", size = 203929, upload-time = "2026-04-05T20:50:51.556Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/92/a4/7b6ab05c18d6c6e682382a0f0235301684452c4131a869f45961d1d032c9/pydantic_extra_types-2.11.2-py3-none-any.whl", hash = "sha256:683b8943252543e49760f89733b1519bc62f31d1a287ebbdc5a7b7959fb4acfd", size = 82851, upload-time = "2026-04-05T20:50:50.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1929,15 +1957,15 @@ hiredis = [
|
||||
|
||||
[[package]]
|
||||
name = "reportlab"
|
||||
version = "4.4.10"
|
||||
version = "4.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "pillow" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/57/28bfbf0a775b618b6e4d854ef8dd3f5c8988e5d614d8898703502a35f61c/reportlab-4.4.10.tar.gz", hash = "sha256:5cbbb34ac3546039d0086deb2938cdec06b12da3cdb836e813258eb33cd28487", size = 3714962, upload-time = "2026-02-12T10:45:21.325Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/23/b8a8b9a5e596ce3de71237c8d6c6a976c763e930878b16340aff3d67ed53/reportlab-4.5.0.tar.gz", hash = "sha256:e595932789ab7a107ba253e83f7815622708a9fd49920d0d6a909880eb66ac75", size = 3914127, upload-time = "2026-04-29T09:12:26.785Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/2e/e1798b8b248e1517e74c6cdf10dd6edd485044e7edf46b5f11ffcc5a0add/reportlab-4.4.10-py3-none-any.whl", hash = "sha256:5abc815746ae2bc44e7ff25db96814f921349ca814c992c7eac3c26029bf7c24", size = 1955400, upload-time = "2026-02-12T10:45:18.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/13/bc43591a54dd38ac5c19e7e849f0311d879737a0d07e032e5be79849a5fb/reportlab-4.5.0-py3-none-any.whl", hash = "sha256:b8cc8996947d84e805368b47b2376070966f091d029351a0d8a1f238984c2c7f", size = 1957238, upload-time = "2026-04-29T09:12:22.904Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1995,40 +2023,40 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.9"
|
||||
version = "0.15.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.57.0"
|
||||
version = "2.58.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4f/87/46c0406d8b5ddd026f73adaf5ab75ce144219c41a4830b52df4b9ab55f7f/sentry_sdk-2.57.0.tar.gz", hash = "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199", size = 435288, upload-time = "2026-03-31T09:39:29.264Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/b3/fb8291170d0e844173164709fc0fa0c221ed75a5da740c8746f2a83b4eb1/sentry_sdk-2.58.0.tar.gz", hash = "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f", size = 438764, upload-time = "2026-04-13T17:23:26.265Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/64/982e07b93219cb52e1cca5d272cb579e2f3eb001956c9e7a9a6d106c9473/sentry_sdk-2.57.0-py2.py3-none-any.whl", hash = "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585", size = 456489, upload-time = "2026-03-31T09:39:27.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2053,7 +2081,8 @@ dependencies = [
|
||||
{ name = "django-simple-captcha" },
|
||||
{ name = "environs", extra = ["django"] },
|
||||
{ name = "honcho" },
|
||||
{ name = "ical" },
|
||||
{ name = "ical", version = "12.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" },
|
||||
{ name = "ical", version = "13.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "libsass" },
|
||||
{ name = "mistune" },
|
||||
@@ -2104,7 +2133,7 @@ tests = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "celery", extras = ["redis"], specifier = ">=5.6.2,<7" },
|
||||
{ name = "cryptography", specifier = ">=46.0.5,<47.0.0" },
|
||||
{ name = "cryptography", specifier = ">=47.0.0,<48.0.0" },
|
||||
{ name = "dict2xml", specifier = ">=1.7.8,<2.0.0" },
|
||||
{ name = "django", specifier = ">=5.2.13,<6.0.0" },
|
||||
{ name = "django-celery-beat", specifier = ">=2.9.0" },
|
||||
@@ -2113,56 +2142,56 @@ requires-dist = [
|
||||
{ name = "django-haystack", specifier = ">=3.3.0,<4.0.0" },
|
||||
{ name = "django-honeypot", specifier = ">=1.3.0,<2" },
|
||||
{ name = "django-jinja", specifier = ">=2.11.0,<3.0.0" },
|
||||
{ name = "django-ninja", specifier = ">=1.5.3,<6.0.0" },
|
||||
{ name = "django-ninja-extra", specifier = ">=0.31.0" },
|
||||
{ name = "django-ninja", specifier = ">=1.6.2,<6.0.0" },
|
||||
{ name = "django-ninja-extra", specifier = ">=0.31.4" },
|
||||
{ name = "django-ordered-model", specifier = ">=3.7.4,<4.0.0" },
|
||||
{ name = "django-phonenumber-field", specifier = ">=8.4.0,<9.0.0" },
|
||||
{ name = "django-simple-captcha", specifier = ">=0.6.3,<1.0.0" },
|
||||
{ name = "environs", extras = ["django"], specifier = ">=14.5.0,<16.0.0" },
|
||||
{ name = "environs", extras = ["django"], specifier = ">=15.0.1,<16.0.0" },
|
||||
{ name = "honcho", specifier = ">=2.0.0" },
|
||||
{ name = "ical", specifier = ">=11.1.0,<12" },
|
||||
{ name = "ical", specifier = ">=11.1.0,<14.0.0" },
|
||||
{ name = "jinja2", specifier = ">=3.1.6,<4.0.0" },
|
||||
{ name = "libsass", specifier = ">=0.23.0,<1.0.0" },
|
||||
{ name = "mistune", specifier = ">=3.2.0,<4.0.0" },
|
||||
{ name = "phonenumbers", specifier = ">=9.0.25,<10.0.0" },
|
||||
{ name = "pillow", specifier = ">=12.1.1,<13.0.0" },
|
||||
{ name = "phonenumbers", specifier = ">=9.0.29,<10.0.0" },
|
||||
{ name = "pillow", specifier = ">=12.2.0,<13.0.0" },
|
||||
{ name = "psutil", specifier = ">=7.2.2,<8.0.0" },
|
||||
{ name = "pydantic-extra-types", specifier = ">=2.11.0,<3.0.0" },
|
||||
{ name = "pydantic-extra-types", specifier = ">=2.11.1,<3.0.0" },
|
||||
{ name = "python-dateutil", specifier = ">=2.9.0.post0,<3.0.0.0" },
|
||||
{ name = "redis", extras = ["hiredis"], specifier = ">=5.3.0,<8.0.0" },
|
||||
{ name = "reportlab", specifier = ">=4.4.10,<5.0.0" },
|
||||
{ name = "redis", extras = ["hiredis"], specifier = ">=6.4.0,<8.0.0" },
|
||||
{ name = "reportlab", specifier = ">=4.5.0,<5.0.0" },
|
||||
{ name = "requests", specifier = ">=2.32.5,<3.0.0" },
|
||||
{ name = "sentry-sdk", specifier = ">=2.54.0,<3.0.0" },
|
||||
{ name = "sentry-sdk", specifier = ">=2.58.0,<3.0.0" },
|
||||
{ name = "sphinx", specifier = ">=5,<6" },
|
||||
{ name = "tomli", specifier = ">=2.4.0,<3.0.0" },
|
||||
{ name = "xapian-haystack", specifier = "<3.1.0,<5.0.0" },
|
||||
{ name = "tomli", specifier = ">=2.4.1,<3.0.0" },
|
||||
{ name = "xapian-haystack", specifier = ">=4.0.0,<5.0.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "django-debug-toolbar", specifier = ">=6.2.0,<7" },
|
||||
{ name = "djhtml", specifier = ">=3.0.10,<4.0.0" },
|
||||
{ name = "faker", specifier = ">=40.8.0,<41.0.0" },
|
||||
{ name = "ipython", specifier = ">=9.11.0,<10.0.0" },
|
||||
{ name = "pre-commit", specifier = ">=4.5.1,<5.0.0" },
|
||||
{ name = "django-debug-toolbar", specifier = ">=6.3.0,<7" },
|
||||
{ name = "djhtml", specifier = ">=3.0.11,<4.0.0" },
|
||||
{ name = "faker", specifier = ">=40.15.0,<41.0.0" },
|
||||
{ name = "ipython", specifier = ">=9.13.0,<10.0.0" },
|
||||
{ name = "pre-commit", specifier = ">=4.6.0,<5.0.0" },
|
||||
{ name = "rjsmin", specifier = ">=1.2.5,<2.0.0" },
|
||||
{ name = "ruff", specifier = ">=0.15.5,<1.0.0" },
|
||||
{ name = "ruff", specifier = ">=0.15.12,<1.0.0" },
|
||||
]
|
||||
docs = [
|
||||
{ name = "mkdocs", specifier = ">=1.6.1,<2.0.0" },
|
||||
{ name = "mkdocs-include-markdown-plugin", specifier = ">=7.2.1,<8.0.0" },
|
||||
{ name = "mkdocs-material", specifier = ">=9.7.5,<10.0.0" },
|
||||
{ name = "mkdocstrings", specifier = ">=1.0.3,<2.0.0" },
|
||||
{ name = "mkdocs-include-markdown-plugin", specifier = ">=7.2.2,<8.0.0" },
|
||||
{ name = "mkdocs-material", specifier = ">=9.7.6,<10.0.0" },
|
||||
{ name = "mkdocstrings", specifier = ">=1.0.4,<2.0.0" },
|
||||
{ name = "mkdocstrings-python", specifier = ">=2.0.3,<3.0.0" },
|
||||
]
|
||||
prod = [{ name = "psycopg", extras = ["c"], specifier = ">=3.3.3,<4.0.0" }]
|
||||
tests = [
|
||||
{ name = "beautifulsoup4", specifier = ">=4.14.3,<5" },
|
||||
{ name = "freezegun", specifier = ">=1.5.5,<2.0.0" },
|
||||
{ name = "lxml", specifier = ">=6.0.2,<7" },
|
||||
{ name = "model-bakery", specifier = ">=1.23.3,<2.0.0" },
|
||||
{ name = "pytest", specifier = ">=9.0.2,<10.0.0" },
|
||||
{ name = "pytest-cov", specifier = ">=7.0.0,<8.0.0" },
|
||||
{ name = "lxml", specifier = ">=6.1.0,<7" },
|
||||
{ name = "model-bakery", specifier = ">=1.23.4,<2.0.0" },
|
||||
{ name = "pytest", specifier = ">=9.0.3,<10.0.0" },
|
||||
{ name = "pytest-cov", specifier = ">=7.1.0,<8.0.0" },
|
||||
{ name = "pytest-django", specifier = ">=4.12.0,<5.0.0" },
|
||||
]
|
||||
|
||||
@@ -2374,11 +2403,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2026.1"
|
||||
version = "2026.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2413,7 +2442,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "21.2.0"
|
||||
version = "21.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
@@ -2421,9 +2450,9 @@ dependencies = [
|
||||
{ name = "platformdirs" },
|
||||
{ name = "python-discovery" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/aa/92/58199fe10049f9703c2666e809c4f686c54ef0a68b0f6afccf518c0b1eb9/virtualenv-21.2.0.tar.gz", hash = "sha256:1720dc3a62ef5b443092e3f499228599045d7fea4c79199770499df8becf9098", size = 5840618, upload-time = "2026-03-09T17:24:38.013Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3f/8b/6331f7a7fe70131c301106ec1e7cf23e2501bf7d4ca3636805801ca191bb/virtualenv-21.3.0.tar.gz", hash = "sha256:733750db978ec95c2d8eb4feadaa57091002bce404cb39ba69899cf7bd28944e", size = 7614069, upload-time = "2026-04-27T17:05:58.927Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/59/7d02447a55b2e55755011a647479041bc92a82e143f96a8195cb33bd0a1c/virtualenv-21.2.0-py3-none-any.whl", hash = "sha256:1bd755b504931164a5a496d217c014d098426cddc79363ad66ac78125f9d908f", size = 5825084, upload-time = "2026-03-09T17:24:35.378Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/eb/03bfb1299d4c4510329e470f13f9a4ce793df7fcb5a2fd3510f911066f61/virtualenv-21.3.0-py3-none-any.whl", hash = "sha256:4d28ee41f6d9ec8f1f00cd472b9ffbcedda1b3d3b9a575b5c94a2d004fd51bd7", size = 7594690, upload-time = "2026-04-27T17:05:55.468Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2473,10 +2502,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "xapian-haystack"
|
||||
version = "3.0.1"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "django-haystack" },
|
||||
{ name = "filelock" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/65/d947ed25472b5c32011d9f750f3969c82f80e68243a88b3241fd002f6ba4/xapian_haystack-4.0.0.tar.gz", hash = "sha256:db4917cace93ecf3f321b96f974e4f0798936e1f883a2ab17f14112d3bfede0c", size = 41206, upload-time = "2026-04-03T08:49:19.019Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/b4/303c3a31a0d110a898b393d1092afd2b9d9b18d1ace18a1c30b6bb4c8fe4/xapian_haystack-4.0.0-py3-none-any.whl", hash = "sha256:e9a0a6a2a59402c2f1411b803c2b769f1abb085fb30b49e2b9f43e3b642850c3", size = 24642, upload-time = "2026-04-03T08:49:17.414Z" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a0/dd/6f0a4e9046919b2b18551fcb3dba781e3530c44129febe4d185e7cb516a5/xapian-haystack-3.0.1.tar.gz", hash = "sha256:a5c0e1262b95008df4dfeb58d093c654acee3f2b27ea3f7d366900895cdc70f9", size = 36573, upload-time = "2021-11-13T08:57:44.547Z" }
|
||||
|
||||
Reference in New Issue
Block a user