Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0bb912768c
|
|||
|
d3004237b1
|
|||
|
1a060f1091
|
|||
|
19c41f2eed
|
|||
|
fd1f6c54a4
|
|||
|
aeb38b3ed4
|
|||
|
ad0d061b82
|
|||
|
20c48b3989
|
|||
|
931bd2d63e
|
|||
|
061222558b
|
|||
|
0c4d1227bd
|
@@ -0,0 +1,72 @@
|
|||||||
|
name: Run tests
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
debug_sql:
|
||||||
|
description: "Print SQL statements executed by SQLAlchemy during tests"
|
||||||
|
type: boolean
|
||||||
|
default: True
|
||||||
|
|
||||||
|
env:
|
||||||
|
DEBUG_SQL: ${{ inputs.debug_sql || true }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
run-tests:
|
||||||
|
runs-on: debian-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v7
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --locked --group test
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
continue-on-error: true
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
set -x
|
||||||
|
|
||||||
|
PYTEST_ARGS=(
|
||||||
|
-vv
|
||||||
|
)
|
||||||
|
|
||||||
|
if [ "$DEBUG_SQL" == "true" ]; then
|
||||||
|
PYTEST_ARGS+=(
|
||||||
|
--debug-sql
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
uv run -- pytest "${PYTEST_ARGS[@]}"
|
||||||
|
|
||||||
|
- name: Generate badge
|
||||||
|
run: uv run -- coverage-badge -o htmlcov/badge.svg
|
||||||
|
|
||||||
|
- name: Upload test report
|
||||||
|
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1
|
||||||
|
with:
|
||||||
|
source: ./test-report/
|
||||||
|
target: ${{ gitea.ref_name }}/test-report/
|
||||||
|
username: gitea-web
|
||||||
|
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
|
||||||
|
host: pages.pvv.ntnu.no
|
||||||
|
known-hosts: "pages.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2QjfFB+city1SYqltkVqWACfo1j37k+oQQfj13mtgg"
|
||||||
|
|
||||||
|
- name: Upload coverage report
|
||||||
|
uses: https://git.pvv.ntnu.no/Projects/rsync-action@v1
|
||||||
|
with:
|
||||||
|
source: ./htmlcov/
|
||||||
|
target: ${{ gitea.ref_name }}/coverage/
|
||||||
|
username: gitea-web
|
||||||
|
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
|
||||||
|
host: pages.pvv.ntnu.no
|
||||||
|
known-hosts: "pages.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2QjfFB+city1SYqltkVqWACfo1j37k+oQQfj13mtgg"
|
||||||
|
|
||||||
|
- name: Check failure
|
||||||
|
if: failure()
|
||||||
|
run: |
|
||||||
|
echo "Tests failed"
|
||||||
|
exit 1
|
||||||
@@ -25,3 +25,5 @@ result
|
|||||||
config.toml
|
config.toml
|
||||||
|
|
||||||
*.qcow2
|
*.qcow2
|
||||||
|
|
||||||
|
test-report/
|
||||||
|
|||||||
@@ -43,6 +43,12 @@
|
|||||||
python-dotenv
|
python-dotenv
|
||||||
requests
|
requests
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
|
|
||||||
|
pytest
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
pytest-html
|
||||||
|
sqlparse
|
||||||
];
|
];
|
||||||
|
|
||||||
in {
|
in {
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ dev = [
|
|||||||
"werkzeug",
|
"werkzeug",
|
||||||
"poethepoet",
|
"poethepoet",
|
||||||
]
|
]
|
||||||
|
test = [
|
||||||
|
"coverage-badge>=1.1.2",
|
||||||
|
"pytest>=9.0.3",
|
||||||
|
"pytest-cov>=7.1.0",
|
||||||
|
"pytest-html>=4.2.0",
|
||||||
|
"sqlparse>=0.5.5",
|
||||||
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
worblehat = "worblehat:main"
|
worblehat = "worblehat:main"
|
||||||
@@ -84,3 +91,15 @@ ignore = [
|
|||||||
"S101", # assert detected
|
"S101", # assert detected
|
||||||
# "S311", # non-cryptographic random generator
|
# "S311", # non-cryptographic random generator
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
addopts = [
|
||||||
|
# "--cov=dibbler.lib",
|
||||||
|
# "--cov=dibbler.models",
|
||||||
|
# "--cov=dibbler.queries",
|
||||||
|
"--cov-report=html",
|
||||||
|
"--cov-branch",
|
||||||
|
|
||||||
|
"--self-contained-html",
|
||||||
|
"--html=./test-report/index.html",
|
||||||
|
]
|
||||||
|
|||||||
+8
-5
@@ -1,4 +1,5 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
# TODO: Add more languages
|
# TODO: Add more languages
|
||||||
LANGUAGES: set[str] = {
|
LANGUAGES: set[str] = {
|
||||||
@@ -19,12 +20,14 @@ LANGUAGES: set[str] = {
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BookMetadata:
|
class BookData:
|
||||||
"""A class representing metadata for a book."""
|
"""
|
||||||
|
A class representing metadata for a book that we might want to fetch from external sources
|
||||||
|
"""
|
||||||
|
|
||||||
isbn: str
|
isbn: str
|
||||||
title: str
|
title: str
|
||||||
# The source of the metadata provider
|
# ID of the data fetcher used to fetch this instance
|
||||||
source: str
|
source: str
|
||||||
authors: set[str]
|
authors: set[str]
|
||||||
language: str | None
|
language: str | None
|
||||||
@@ -32,11 +35,11 @@ class BookMetadata:
|
|||||||
num_pages: int | None
|
num_pages: int | None
|
||||||
subjects: set[str]
|
subjects: set[str]
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, any]:
|
def to_dict(self) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
"isbn": self.isbn,
|
"isbn": self.isbn,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"source": self.metadata_source_id(),
|
"source": self.source,
|
||||||
"authors": set() if self.authors is None else self.authors,
|
"authors": set() if self.authors is None else self.authors,
|
||||||
"language": self.language,
|
"language": self.language,
|
||||||
"publish_date": self.publish_date,
|
"publish_date": self.publish_date,
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# base fetcher.
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
from .BookData import BookData
|
||||||
|
|
||||||
|
|
||||||
|
class BookDataFetcher(ABC):
|
||||||
|
"""
|
||||||
|
A base class for adapters that fetch book data from external sources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def fetcher_id(cls) -> str:
|
||||||
|
"""Returns a unique identifier for this specific fetcher, to identify where the data came from."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@abstractmethod
|
||||||
|
def try_fetch_data(cls, isbn: str) -> BookData | None:
|
||||||
|
"""Tries to fetch data for the given ISBN."""
|
||||||
|
pass
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .book_data_fetcher import fetch_book_data_from_multiple_sources
|
||||||
|
|
||||||
|
__all__ = ["fetch_book_data_from_multiple_sources"]
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""
|
||||||
|
this module contains the fetch_book_data_from_multiple_sources() function which combines all fetchers and returns ranked results (if any)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
from worblehat.book_data_fetchers.BookData import BookData
|
||||||
|
from worblehat.book_data_fetchers.BookDataFetcher import BookDataFetcher
|
||||||
|
from worblehat.book_data_fetchers.fetchers.GoogleBooksFetcher import GoogleBooksFetcher
|
||||||
|
from worblehat.book_data_fetchers.fetchers.OpenLibraryFetcher import OpenLibraryFetcher
|
||||||
|
from worblehat.book_data_fetchers.fetchers.OutlandScraperFetcher import (
|
||||||
|
OutlandScraperFetcher,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The order of these fetchers determines the priority of the sources.
|
||||||
|
# The first fetcher in the list has the highest priority.
|
||||||
|
FETCHERS: list[BookDataFetcher] = [
|
||||||
|
OpenLibraryFetcher,
|
||||||
|
GoogleBooksFetcher,
|
||||||
|
OutlandScraperFetcher,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
FETCHER_SOURCE_IDS: list[str] = [fetcher.fetcher_id() for fetcher in FETCHERS]
|
||||||
|
|
||||||
|
|
||||||
|
def sort_data_by_priority(data: list[BookData]) -> list[BookData]:
|
||||||
|
"""
|
||||||
|
Sorts the given data by the priority of the sources.
|
||||||
|
|
||||||
|
The order of the data is the same as the order of the sources in the FETCHERS list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Note that this function is O(n^2) but the number of fetchers is small so it's fine.
|
||||||
|
return sorted(data, key=lambda m: FETCHER_SOURCE_IDS.index(m.source))
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_book_data_from_multiple_sources(isbn: str, strict: bool=False) -> list[BookData]:
|
||||||
|
"""
|
||||||
|
Returns a list of data fetched from multiple fetchers.
|
||||||
|
|
||||||
|
Fetchers that are not able to retrieve any data for the given ISBN will be ignored.
|
||||||
|
|
||||||
|
There is no guarantee that there will be any book data.
|
||||||
|
|
||||||
|
The results are always ordered in the same way as the fetchers are listed in the FETCHERS list.
|
||||||
|
"""
|
||||||
|
isbn = isbn.replace("-", "").replace("_", "").strip().lower()
|
||||||
|
if len(isbn) != 10 and len(isbn) != 13 and not isbn.isnumeric():
|
||||||
|
raise ValueError("Invalid ISBN")
|
||||||
|
|
||||||
|
results: list[BookData] = []
|
||||||
|
|
||||||
|
with ThreadPoolExecutor() as executor:
|
||||||
|
futures = [executor.submit(fetcher.try_fetch_data, isbn) for fetcher in FETCHERS]
|
||||||
|
|
||||||
|
for future in futures:
|
||||||
|
result = future.result()
|
||||||
|
if result is not None:
|
||||||
|
results.append(result)
|
||||||
|
|
||||||
|
for result in results:
|
||||||
|
try:
|
||||||
|
result.validate()
|
||||||
|
except ValueError as e:
|
||||||
|
if strict:
|
||||||
|
raise e
|
||||||
|
print(f"Invalid data: {e}")
|
||||||
|
results.remove(result)
|
||||||
|
|
||||||
|
return sort_data_by_priority(results)
|
||||||
+12
-17
@@ -4,48 +4,43 @@ A BookMetadataFetcher for the Google Books API.
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
from worblehat.book_data_fetchers.BookData import BookData
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
from worblehat.book_data_fetchers.BookDataFetcher import BookDataFetcher
|
||||||
|
|
||||||
|
|
||||||
class GoogleBooksFetcher(BookMetadataFetcher):
|
class GoogleBooksFetcher(BookDataFetcher):
|
||||||
@classmethod
|
@classmethod
|
||||||
def metadata_source_id(_cls) -> str:
|
def fetcher_id(_cls) -> str:
|
||||||
return "google_books"
|
return "google_books"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
|
def try_fetch_data(cls, isbn: str) -> BookData | None:
|
||||||
try:
|
try:
|
||||||
jsonInput = requests.get(
|
jsonInput = requests.get(
|
||||||
"https://www.googleapis.com/books/v1/volumes",
|
"https://www.googleapis.com/books/v1/volumes",
|
||||||
params={"q": f"isbn:{isbn}"},
|
params={"q": f"isbn:{isbn}"},
|
||||||
|
timeout=30,
|
||||||
).json()
|
).json()
|
||||||
data = jsonInput.get("items")[0].get("volumeInfo")
|
data = jsonInput.get("items")[0].get("volumeInfo")
|
||||||
|
|
||||||
authors = set(data.get("authors") or [])
|
authors = set(data.get("authors") or [])
|
||||||
title = data.get("title")
|
title = data.get("title")
|
||||||
publishDate = data.get("publish_date")
|
publishDate = data.get("publishedDate")
|
||||||
numberOfPages = data.get("number_of_pages")
|
numberOfPages = data.get("pageCount")
|
||||||
if numberOfPages:
|
if numberOfPages:
|
||||||
numberOfPages = int(numberOfPages)
|
numberOfPages = int(numberOfPages)
|
||||||
subjects = set(data.get("categories") or [])
|
subjects = set(data.get("categories") or [])
|
||||||
languages = data.get("languages")
|
language = data.get("language")
|
||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return BookMetadata(
|
return BookData(
|
||||||
isbn=isbn,
|
isbn=isbn,
|
||||||
title=title,
|
title=title,
|
||||||
source=cls.metadata_source_id(),
|
source=cls.fetcher_id(),
|
||||||
authors=authors,
|
authors=authors,
|
||||||
language=languages,
|
language=language,
|
||||||
publish_date=publishDate,
|
publish_date=publishDate,
|
||||||
num_pages=numberOfPages,
|
num_pages=numberOfPages,
|
||||||
subjects=subjects,
|
subjects=subjects,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
book_data = GoogleBooksFetcher.fetch_metadata("0132624788")
|
|
||||||
book_data.validate()
|
|
||||||
print(book_data)
|
|
||||||
+7
-13
@@ -4,21 +4,21 @@ A BookMetadataFetcher for the Open Library API.
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
from worblehat.book_data_fetchers.BookData import BookData
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
from worblehat.book_data_fetchers.BookDataFetcher import BookDataFetcher
|
||||||
|
|
||||||
LANGUAGE_MAP = {
|
LANGUAGE_MAP = {
|
||||||
"Norwegian": "no",
|
"Norwegian": "no",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class OpenLibraryFetcher(BookMetadataFetcher):
|
class OpenLibraryFetcher(BookDataFetcher):
|
||||||
@classmethod
|
@classmethod
|
||||||
def metadata_source_id(_cls) -> str:
|
def fetcher_id(_cls) -> str:
|
||||||
return "open_library"
|
return "open_library"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
|
def try_fetch_data(cls, isbn: str) -> BookData | None:
|
||||||
try:
|
try:
|
||||||
jsonInput = requests.get(f"https://openlibrary.org/isbn/{isbn}.json").json()
|
jsonInput = requests.get(f"https://openlibrary.org/isbn/{isbn}.json").json()
|
||||||
|
|
||||||
@@ -48,19 +48,13 @@ class OpenLibraryFetcher(BookMetadataFetcher):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return BookMetadata(
|
return BookData(
|
||||||
isbn=isbn,
|
isbn=isbn,
|
||||||
title=title,
|
title=title,
|
||||||
source=cls.metadata_source_id(),
|
source=cls.fetcher_id(),
|
||||||
authors=author_names,
|
authors=author_names,
|
||||||
language=language,
|
language=language,
|
||||||
publish_date=publishDate,
|
publish_date=publishDate,
|
||||||
num_pages=numberOfPages,
|
num_pages=numberOfPages,
|
||||||
subjects=subjects,
|
subjects=subjects,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
book_data = OpenLibraryFetcher.fetch_metadata("9788205530751")
|
|
||||||
book_data.validate()
|
|
||||||
print(book_data)
|
|
||||||
+7
-13
@@ -5,8 +5,8 @@ A BookMetadataFetcher that webscrapes https://outland.no/
|
|||||||
import requests
|
import requests
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
from worblehat.book_data_fetchers.BookData import BookData
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
from worblehat.book_data_fetchers.BookDataFetcher import BookDataFetcher
|
||||||
|
|
||||||
LANGUAGE_MAP = {
|
LANGUAGE_MAP = {
|
||||||
"Norsk": "no",
|
"Norsk": "no",
|
||||||
@@ -25,13 +25,13 @@ LANGUAGE_MAP = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class OutlandScraperFetcher(BookMetadataFetcher):
|
class OutlandScraperFetcher(BookDataFetcher):
|
||||||
@classmethod
|
@classmethod
|
||||||
def metadata_source_id(_cls) -> str:
|
def fetcher_id(_cls) -> str:
|
||||||
return "outland_scraper"
|
return "outland_scraper"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
|
def try_fetch_data(cls, isbn: str) -> BookData | None:
|
||||||
try:
|
try:
|
||||||
# Find the link to the product page
|
# Find the link to the product page
|
||||||
response = requests.get(f"https://outland.no/{isbn}")
|
response = requests.get(f"https://outland.no/{isbn}")
|
||||||
@@ -89,19 +89,13 @@ class OutlandScraperFetcher(BookMetadataFetcher):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return BookMetadata(
|
return BookData(
|
||||||
isbn=isbn,
|
isbn=isbn,
|
||||||
title=bookData.get("Title"),
|
title=bookData.get("Title"),
|
||||||
source=cls.metadata_source_id(),
|
source=cls.fetcher_id(),
|
||||||
authors=bookData.get("Authors"),
|
authors=bookData.get("Authors"),
|
||||||
language=bookData.get("Language"),
|
language=bookData.get("Language"),
|
||||||
publish_date=bookData.get("PublishDate"),
|
publish_date=bookData.get("PublishDate"),
|
||||||
num_pages=bookData.get("NumberOfPages"),
|
num_pages=bookData.get("NumberOfPages"),
|
||||||
subjects=bookData.get("Subjects"),
|
subjects=bookData.get("Subjects"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
book_data = OutlandScraperFetcher.fetch_metadata("9781947808225")
|
|
||||||
book_data.validate()
|
|
||||||
print(book_data)
|
|
||||||
+16
-36
@@ -1,4 +1,3 @@
|
|||||||
from datetime import datetime
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
from libdib.repl import (
|
from libdib.repl import (
|
||||||
@@ -6,13 +5,17 @@ from libdib.repl import (
|
|||||||
NumberedCmd,
|
NumberedCmd,
|
||||||
prompt_yes_no,
|
prompt_yes_no,
|
||||||
)
|
)
|
||||||
from sqlalchemy import (
|
from sqlalchemy import event
|
||||||
event,
|
|
||||||
select,
|
|
||||||
)
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from worblehat.models import *
|
from worblehat.models import *
|
||||||
|
from worblehat.queries import (
|
||||||
|
find_bookcase_item_by_isbn,
|
||||||
|
find_media_type_by_name,
|
||||||
|
list_active_borrowings,
|
||||||
|
list_all_queue_items,
|
||||||
|
list_overdue_borrowings,
|
||||||
|
)
|
||||||
from worblehat.services import (
|
from worblehat.services import (
|
||||||
create_bookcase_item_from_isbn,
|
create_bookcase_item_from_isbn,
|
||||||
is_valid_isbn,
|
is_valid_isbn,
|
||||||
@@ -72,11 +75,7 @@ class WorblehatCli(NumberedCmd):
|
|||||||
print(f" {item.name} - {item.amount} copies")
|
print(f" {item.name} - {item.amount} copies")
|
||||||
|
|
||||||
def do_show_borrowed_queued(self, _: str) -> None:
|
def do_show_borrowed_queued(self, _: str) -> None:
|
||||||
borrowed_items = self.sql_session.scalars(
|
borrowed_items = list_active_borrowings(self.sql_session)
|
||||||
select(BookcaseItemBorrowing)
|
|
||||||
.where(BookcaseItemBorrowing.delivered.is_(None))
|
|
||||||
.order_by(BookcaseItemBorrowing.end_time),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(borrowed_items) == 0:
|
if len(borrowed_items) == 0:
|
||||||
print("No borrowed items found.")
|
print("No borrowed items found.")
|
||||||
@@ -89,11 +88,7 @@ class WorblehatCli(NumberedCmd):
|
|||||||
|
|
||||||
print()
|
print()
|
||||||
|
|
||||||
queued_items = self.sql_session.scalars(
|
queued_items = list_all_queue_items(self.sql_session)
|
||||||
select(BookcaseItemBorrowingQueue).order_by(
|
|
||||||
BookcaseItemBorrowingQueue.entered_queue_time,
|
|
||||||
),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(queued_items) == 0:
|
if len(queued_items) == 0:
|
||||||
print("No queued items found.")
|
print("No queued items found.")
|
||||||
@@ -121,7 +116,6 @@ class WorblehatCli(NumberedCmd):
|
|||||||
"""),
|
"""),
|
||||||
)
|
)
|
||||||
|
|
||||||
with self.sql_session.no_autoflush:
|
|
||||||
print("Please select the bookcase where the item is placed:")
|
print("Please select the bookcase where the item is placed:")
|
||||||
bookcase_selector = InteractiveItemSelector(
|
bookcase_selector = InteractiveItemSelector(
|
||||||
cls=Bookcase,
|
cls=Bookcase,
|
||||||
@@ -138,9 +132,7 @@ class WorblehatCli(NumberedCmd):
|
|||||||
media_type_selector = InteractiveItemSelector(
|
media_type_selector = InteractiveItemSelector(
|
||||||
cls=MediaType,
|
cls=MediaType,
|
||||||
sql_session=self.sql_session,
|
sql_session=self.sql_session,
|
||||||
default=self.sql_session.scalars(
|
default=find_media_type_by_name(self.sql_session, "book"),
|
||||||
select(MediaType).where(MediaType.name.ilike("book")),
|
|
||||||
).one(),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
media_type_selector.cmdloop()
|
media_type_selector.cmdloop()
|
||||||
@@ -162,12 +154,10 @@ class WorblehatCli(NumberedCmd):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if (
|
if (
|
||||||
existing_item := self.sql_session.scalars(
|
existing_item := find_bookcase_item_by_isbn(
|
||||||
select(BookcaseItem)
|
self.sql_session,
|
||||||
.where(BookcaseItem.isbn == isbn)
|
isbn,
|
||||||
.join(BookcaseItemBorrowing)
|
)
|
||||||
.join(BookcaseItemBorrowingQueue),
|
|
||||||
).one_or_none()
|
|
||||||
) is not None:
|
) is not None:
|
||||||
print(f'\nFound existing item for isbn "{isbn}"')
|
print(f'\nFound existing item for isbn "{isbn}"')
|
||||||
BookcaseItemCli(
|
BookcaseItemCli(
|
||||||
@@ -192,17 +182,7 @@ class WorblehatCli(NumberedCmd):
|
|||||||
).cmdloop()
|
).cmdloop()
|
||||||
|
|
||||||
def do_show_slabbedasker(self, _: str) -> None:
|
def do_show_slabbedasker(self, _: str) -> None:
|
||||||
slubberter = self.sql_session.scalars(
|
slubberter = list_overdue_borrowings(self.sql_session)
|
||||||
select(BookcaseItemBorrowing)
|
|
||||||
.join(BookcaseItem)
|
|
||||||
.where(
|
|
||||||
BookcaseItemBorrowing.end_time < datetime.now(),
|
|
||||||
BookcaseItemBorrowing.delivered.is_(None),
|
|
||||||
)
|
|
||||||
.order_by(
|
|
||||||
BookcaseItemBorrowing.end_time,
|
|
||||||
),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(slubberter) == 0:
|
if len(slubberter) == 0:
|
||||||
print("No slubberts found. Life is good.")
|
print("No slubberts found. Life is good.")
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ from libdib.repl import (
|
|||||||
InteractiveItemSelector,
|
InteractiveItemSelector,
|
||||||
NumberedCmd,
|
NumberedCmd,
|
||||||
)
|
)
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from worblehat.models import Bookcase, BookcaseShelf
|
from worblehat.models import Bookcase, BookcaseShelf
|
||||||
|
from worblehat.queries import (
|
||||||
|
find_bookcase_by_name,
|
||||||
|
find_bookcase_shelf,
|
||||||
|
list_bookcase_shelfs_ordered,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdvancedOptionsCli(NumberedCmd):
|
class AdvancedOptionsCli(NumberedCmd):
|
||||||
@@ -20,12 +24,7 @@ class AdvancedOptionsCli(NumberedCmd):
|
|||||||
print("Error: name cannot be empty")
|
print("Error: name cannot be empty")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (
|
if find_bookcase_by_name(self.sql_session, name) is not None:
|
||||||
self.sql_session.scalars(
|
|
||||||
select(Bookcase).where(Bookcase.name == name),
|
|
||||||
).one_or_none()
|
|
||||||
is not None
|
|
||||||
):
|
|
||||||
print(f"Error: a bookcase with name {name} already exists")
|
print(f"Error: a bookcase with name {name} already exists")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -67,13 +66,7 @@ class AdvancedOptionsCli(NumberedCmd):
|
|||||||
break
|
break
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.sql_session.scalars(
|
find_bookcase_shelf(self.sql_session, bookcase, column, row)
|
||||||
select(BookcaseShelf).where(
|
|
||||||
BookcaseShelf.bookcase == bookcase,
|
|
||||||
BookcaseShelf.column == column,
|
|
||||||
BookcaseShelf.row == row,
|
|
||||||
),
|
|
||||||
).one_or_none()
|
|
||||||
is not None
|
is not None
|
||||||
):
|
):
|
||||||
print(
|
print(
|
||||||
@@ -95,15 +88,7 @@ class AdvancedOptionsCli(NumberedCmd):
|
|||||||
self.sql_session.flush()
|
self.sql_session.flush()
|
||||||
|
|
||||||
def do_list_bookcases(self, _: str) -> None:
|
def do_list_bookcases(self, _: str) -> None:
|
||||||
bookcase_shelfs = self.sql_session.scalars(
|
bookcase_shelfs = list_bookcase_shelfs_ordered(self.sql_session)
|
||||||
select(BookcaseShelf)
|
|
||||||
.join(Bookcase)
|
|
||||||
.order_by(
|
|
||||||
Bookcase.name,
|
|
||||||
BookcaseShelf.column,
|
|
||||||
BookcaseShelf.row,
|
|
||||||
),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
bookcase_uid = None
|
bookcase_uid = None
|
||||||
for shelf in bookcase_shelfs:
|
for shelf in bookcase_shelfs:
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from libdib.repl import (
|
|||||||
format_date,
|
format_date,
|
||||||
prompt_yes_no,
|
prompt_yes_no,
|
||||||
)
|
)
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from worblehat.models import (
|
from worblehat.models import (
|
||||||
@@ -19,6 +18,15 @@ from worblehat.models import (
|
|||||||
Language,
|
Language,
|
||||||
MediaType,
|
MediaType,
|
||||||
)
|
)
|
||||||
|
from worblehat.queries import (
|
||||||
|
find_bookcase_item_by_isbn,
|
||||||
|
find_bookcase_item_by_name,
|
||||||
|
has_active_borrowing,
|
||||||
|
is_in_borrowing_queue,
|
||||||
|
list_active_borrowings_for_item,
|
||||||
|
list_borrowings_for_isbn,
|
||||||
|
list_pending_queue_items_for_item,
|
||||||
|
)
|
||||||
from worblehat.services.bookcase_item import (
|
from worblehat.services.bookcase_item import (
|
||||||
create_bookcase_item_from_isbn,
|
create_bookcase_item_from_isbn,
|
||||||
is_valid_isbn,
|
is_valid_isbn,
|
||||||
@@ -77,37 +85,16 @@ class BookcaseItemCli(NumberedCmd):
|
|||||||
return username
|
return username
|
||||||
|
|
||||||
def _has_active_borrowing(self, username: str) -> bool:
|
def _has_active_borrowing(self, username: str) -> bool:
|
||||||
return (
|
return has_active_borrowing(self.sql_session, username, self.bookcase_item)
|
||||||
self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowing).where(
|
|
||||||
BookcaseItemBorrowing.username == username,
|
|
||||||
BookcaseItemBorrowing.item == self.bookcase_item,
|
|
||||||
BookcaseItemBorrowing.delivered.is_(None),
|
|
||||||
),
|
|
||||||
).one_or_none()
|
|
||||||
is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
def _has_borrowing_queue_item(self, username: str) -> bool:
|
def _has_borrowing_queue_item(self, username: str) -> bool:
|
||||||
return (
|
return is_in_borrowing_queue(self.sql_session, username, self.bookcase_item)
|
||||||
self.sql_session.scalars(
|
|
||||||
select(BookcaseItemBorrowingQueue).where(
|
|
||||||
BookcaseItemBorrowingQueue.username == username,
|
|
||||||
BookcaseItemBorrowingQueue.item == self.bookcase_item,
|
|
||||||
),
|
|
||||||
).one_or_none()
|
|
||||||
is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
def do_borrow(self, _: str) -> None:
|
def do_borrow(self, _: str) -> None:
|
||||||
active_borrowings = self.sql_session.scalars(
|
active_borrowings = list_active_borrowings_for_item(
|
||||||
select(BookcaseItemBorrowing)
|
self.sql_session,
|
||||||
.where(
|
self.bookcase_item,
|
||||||
BookcaseItemBorrowing.item == self.bookcase_item,
|
|
||||||
BookcaseItemBorrowing.delivered.is_(None),
|
|
||||||
)
|
)
|
||||||
.order_by(BookcaseItemBorrowing.end_time),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(active_borrowings) >= self.bookcase_item.amount:
|
if len(active_borrowings) >= self.bookcase_item.amount:
|
||||||
print("This item is currently not available")
|
print("This item is currently not available")
|
||||||
@@ -157,15 +144,7 @@ class BookcaseItemCli(NumberedCmd):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def do_deliver(self, _: str) -> None:
|
def do_deliver(self, _: str) -> None:
|
||||||
borrowings = self.sql_session.scalars(
|
borrowings = list_borrowings_for_isbn(self.sql_session, self.bookcase_item.isbn)
|
||||||
select(BookcaseItemBorrowing)
|
|
||||||
.join(
|
|
||||||
BookcaseItem,
|
|
||||||
BookcaseItem.uid == BookcaseItemBorrowing.fk_bookcase_item_uid,
|
|
||||||
)
|
|
||||||
.where(BookcaseItem.isbn == self.bookcase_item.isbn)
|
|
||||||
.order_by(BookcaseItemBorrowing.username),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(borrowings) == 0:
|
if len(borrowings) == 0:
|
||||||
print("No one seems to have borrowed this item")
|
print("No one seems to have borrowed this item")
|
||||||
@@ -194,28 +173,16 @@ class BookcaseItemCli(NumberedCmd):
|
|||||||
print(f"Successfully delivered the item for {borrowing.username}")
|
print(f"Successfully delivered the item for {borrowing.username}")
|
||||||
|
|
||||||
def do_extend_borrowing(self, _: str) -> None:
|
def do_extend_borrowing(self, _: str) -> None:
|
||||||
borrowings = self.sql_session.scalars(
|
borrowings = list_borrowings_for_isbn(self.sql_session, self.bookcase_item.isbn)
|
||||||
select(BookcaseItemBorrowing)
|
|
||||||
.join(
|
|
||||||
BookcaseItem,
|
|
||||||
BookcaseItem.uid == BookcaseItemBorrowing.fk_bookcase_item_uid,
|
|
||||||
)
|
|
||||||
.where(BookcaseItem.isbn == self.bookcase_item.isbn)
|
|
||||||
.order_by(BookcaseItemBorrowing.username),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(borrowings) == 0:
|
if len(borrowings) == 0:
|
||||||
print("No one seems to have borrowed this item")
|
print("No one seems to have borrowed this item")
|
||||||
return
|
return
|
||||||
|
|
||||||
borrowing_queue = self.sql_session.scalars(
|
borrowing_queue = list_pending_queue_items_for_item(
|
||||||
select(BookcaseItemBorrowingQueue)
|
self.sql_session,
|
||||||
.where(
|
self.bookcase_item,
|
||||||
BookcaseItemBorrowingQueue.item == self.bookcase_item,
|
|
||||||
BookcaseItemBorrowingQueue.item_became_available_time is None,
|
|
||||||
)
|
)
|
||||||
.order_by(BookcaseItemBorrowingQueue.entered_queue_time),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(borrowing_queue) != 0:
|
if len(borrowing_queue) != 0:
|
||||||
print(
|
print(
|
||||||
@@ -302,12 +269,7 @@ class EditBookcaseCli(NumberedCmd):
|
|||||||
print("Error: name cannot be empty")
|
print("Error: name cannot be empty")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (
|
if find_bookcase_item_by_name(self.sql_session, name) is not None:
|
||||||
self.sql_session.scalars(
|
|
||||||
select(BookcaseItem).where(BookcaseItem.name == name),
|
|
||||||
).one_or_none()
|
|
||||||
is not None
|
|
||||||
):
|
|
||||||
print(f"Error: an item with name {name} already exists")
|
print(f"Error: an item with name {name} already exists")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -326,12 +288,7 @@ class EditBookcaseCli(NumberedCmd):
|
|||||||
print("Error: ISBN is not valid")
|
print("Error: ISBN is not valid")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (
|
if find_bookcase_item_by_isbn(self.sql_session, isbn) is not None:
|
||||||
self.sql_session.scalars(
|
|
||||||
select(BookcaseItem).where(BookcaseItem.isbn == isbn),
|
|
||||||
).one_or_none()
|
|
||||||
is not None
|
|
||||||
):
|
|
||||||
print(f"Error: an item with ISBN {isbn} already exists")
|
print(f"Error: an item with ISBN {isbn} already exists")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
from libdib.repl import InteractiveItemSelector
|
from libdib.repl import InteractiveItemSelector
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from worblehat.models import (
|
from worblehat.models import (
|
||||||
Bookcase,
|
Bookcase,
|
||||||
BookcaseShelf,
|
BookcaseShelf,
|
||||||
)
|
)
|
||||||
|
from worblehat.queries import (
|
||||||
|
find_bookcase_shelf,
|
||||||
|
list_bookcase_shelf_positions,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def select_bookcase_shelf(
|
def select_bookcase_shelf(
|
||||||
@@ -15,29 +18,33 @@ def select_bookcase_shelf(
|
|||||||
) -> BookcaseShelf:
|
) -> BookcaseShelf:
|
||||||
def __complete_bookshelf_selection(session: Session, cls: type, arg: str):
|
def __complete_bookshelf_selection(session: Session, cls: type, arg: str):
|
||||||
args = arg.split("-")
|
args = arg.split("-")
|
||||||
query = select(cls.row, cls.column).where(cls.bookcase == bookcase)
|
column = None
|
||||||
|
row = None
|
||||||
try:
|
try:
|
||||||
if arg != "" and len(args) > 0:
|
if arg != "" and len(args) > 0:
|
||||||
query = query.where(cls.column == int(args[0]))
|
column = int(args[0])
|
||||||
if len(args) > 1:
|
if len(args) > 1:
|
||||||
query = query.where(cls.row == int(args[1]))
|
row = int(args[1])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
result = session.execute(query).all()
|
result = list_bookcase_shelf_positions(session, bookcase, column, row)
|
||||||
return [f"{c}-{r}" for r, c in result]
|
return [f"{c}-{r}" for r, c in result]
|
||||||
|
|
||||||
|
def __execute_bookshelf_selection(session: Session, cls: type, arg: str):
|
||||||
|
shelf = find_bookcase_shelf(
|
||||||
|
session,
|
||||||
|
bookcase,
|
||||||
|
int(arg.split("-")[0]),
|
||||||
|
int(arg.split("-")[1]),
|
||||||
|
)
|
||||||
|
return [shelf] if shelf is not None else []
|
||||||
|
|
||||||
print(prompt)
|
print(prompt)
|
||||||
bookcase_shelf_selector = InteractiveItemSelector(
|
bookcase_shelf_selector = InteractiveItemSelector(
|
||||||
cls=BookcaseShelf,
|
cls=BookcaseShelf,
|
||||||
sql_session=sql_session,
|
sql_session=sql_session,
|
||||||
execute_selection=lambda session, cls, arg: session.scalars(
|
execute_selection=__execute_bookshelf_selection,
|
||||||
select(cls).where(
|
|
||||||
cls.bookcase == bookcase,
|
|
||||||
cls.column == int(arg.split("-")[0]),
|
|
||||||
cls.row == int(arg.split("-")[1]),
|
|
||||||
),
|
|
||||||
).all(),
|
|
||||||
complete_selection=__complete_bookshelf_selection,
|
complete_selection=__complete_bookshelf_selection,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,14 @@ from libdib.repl import (
|
|||||||
NumberedCmd,
|
NumberedCmd,
|
||||||
NumberedItemSelector,
|
NumberedItemSelector,
|
||||||
)
|
)
|
||||||
from sqlalchemy import select
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from worblehat.models import Author, BookcaseItem
|
from worblehat.queries import (
|
||||||
|
list_bookcase_items_by_owner,
|
||||||
|
search_authors_by_name,
|
||||||
|
search_bookcase_item_owners,
|
||||||
|
search_bookcase_items_by_title,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SearchCli(NumberedCmd):
|
class SearchCli(NumberedCmd):
|
||||||
@@ -21,9 +25,7 @@ class SearchCli(NumberedCmd):
|
|||||||
while (input_text := input("Enter title: ")) == "":
|
while (input_text := input("Enter title: ")) == "":
|
||||||
pass
|
pass
|
||||||
|
|
||||||
items = self.sql_session.scalars(
|
items = search_bookcase_items_by_title(self.sql_session, input_text)
|
||||||
select(BookcaseItem).where(BookcaseItem.name.ilike(f"%{input_text}%")),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(items) == 0:
|
if len(items) == 0:
|
||||||
print("No items found.")
|
print("No items found.")
|
||||||
@@ -43,9 +45,7 @@ class SearchCli(NumberedCmd):
|
|||||||
while (input_text := input("Enter author name: ")) == "":
|
while (input_text := input("Enter author name: ")) == "":
|
||||||
pass
|
pass
|
||||||
|
|
||||||
author = self.sql_session.scalars(
|
author = search_authors_by_name(self.sql_session, input_text)
|
||||||
select(Author).where(Author.name.ilike(f"%{input_text}%")),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(author) == 0:
|
if len(author) == 0:
|
||||||
print("No authors found.")
|
print("No authors found.")
|
||||||
@@ -80,11 +80,7 @@ class SearchCli(NumberedCmd):
|
|||||||
while (input_text := input("Enter username: ")) == "":
|
while (input_text := input("Enter username: ")) == "":
|
||||||
pass
|
pass
|
||||||
|
|
||||||
users = self.sql_session.scalars(
|
users = search_bookcase_item_owners(self.sql_session, input_text)
|
||||||
select(BookcaseItem.owner)
|
|
||||||
.where(BookcaseItem.owner.ilike(f"%{input_text}%"))
|
|
||||||
.distinct(),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
if len(users) == 0:
|
if len(users) == 0:
|
||||||
print("No users found.")
|
print("No users found.")
|
||||||
@@ -100,9 +96,7 @@ class SearchCli(NumberedCmd):
|
|||||||
return None
|
return None
|
||||||
selected_user = selector.result
|
selected_user = selector.result
|
||||||
|
|
||||||
items = self.sql_session.scalars(
|
items = list_bookcase_items_by_owner(self.sql_session, selected_user)
|
||||||
select(BookcaseItem).where(BookcaseItem.owner == selected_user),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
selector = NumberedItemSelector(
|
selector = NumberedItemSelector(
|
||||||
items=items,
|
items=items,
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import logging
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
|
||||||
from sqlalchemy import func, select
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from worblehat.models import (
|
from worblehat.models import (
|
||||||
@@ -10,6 +9,15 @@ from worblehat.models import (
|
|||||||
BookcaseItemBorrowingQueue,
|
BookcaseItemBorrowingQueue,
|
||||||
DeadlineDaemonLastRunDatetime,
|
DeadlineDaemonLastRunDatetime,
|
||||||
)
|
)
|
||||||
|
from worblehat.queries import (
|
||||||
|
find_last_run,
|
||||||
|
find_next_queue_position,
|
||||||
|
list_close_deadline_borrowings,
|
||||||
|
list_expiring_queue_positions,
|
||||||
|
list_newly_available_queue_items,
|
||||||
|
list_overdue_queue_positions,
|
||||||
|
list_undelivered_overdue_borrowings,
|
||||||
|
)
|
||||||
from worblehat.services.config import Config
|
from worblehat.services.config import Config
|
||||||
from worblehat.services.email import send_email
|
from worblehat.services.email import send_email
|
||||||
|
|
||||||
@@ -21,9 +29,7 @@ class DeadlineDaemon:
|
|||||||
|
|
||||||
self.sql_session = sql_session
|
self.sql_session = sql_session
|
||||||
|
|
||||||
self.last_run = self.sql_session.scalars(
|
self.last_run = find_last_run(self.sql_session)
|
||||||
select(DeadlineDaemonLastRunDatetime),
|
|
||||||
).one_or_none()
|
|
||||||
|
|
||||||
if self.last_run is None:
|
if self.last_run is None:
|
||||||
logging.info("No previous run found, assuming this is the first run")
|
logging.info("No previous run found, assuming this is the first run")
|
||||||
@@ -158,16 +164,6 @@ class DeadlineDaemon:
|
|||||||
# EMAIL ROUTINES #
|
# EMAIL ROUTINES #
|
||||||
##################
|
##################
|
||||||
|
|
||||||
def _sql_subtract_date(self, x: datetime, y: timedelta):
|
|
||||||
if self.sql_session.bind.dialect.name == "sqlite":
|
|
||||||
# SQLite does not support timedelta in queries
|
|
||||||
return func.datetime(x, f"-{y.days} days")
|
|
||||||
if self.sql_session.bind.dialect.name == "postgresql":
|
|
||||||
return x - y
|
|
||||||
raise NotImplementedError(
|
|
||||||
f"Unsupported dialect: {self.sql_session.bind.dialect.name}",
|
|
||||||
)
|
|
||||||
|
|
||||||
def send_close_deadline_reminder_mails(self) -> None:
|
def send_close_deadline_reminder_mails(self) -> None:
|
||||||
logging.info("Sending mails for items with a closing deadline")
|
logging.info("Sending mails for items with a closing deadline")
|
||||||
|
|
||||||
@@ -175,30 +171,22 @@ class DeadlineDaemon:
|
|||||||
days = [int(d) for d in Config["deadline_daemon.warn_days_before_borrowing_deadline"]]
|
days = [int(d) for d in Config["deadline_daemon.warn_days_before_borrowing_deadline"]]
|
||||||
|
|
||||||
for day in days:
|
for day in days:
|
||||||
borrowings_to_remind = self.sql_session.scalars(
|
borrowings_to_remind = list_close_deadline_borrowings(
|
||||||
select(BookcaseItemBorrowing).where(
|
self.sql_session,
|
||||||
self._sql_subtract_date(
|
day,
|
||||||
BookcaseItemBorrowing.end_time,
|
|
||||||
timedelta(days=day),
|
|
||||||
).between(
|
|
||||||
self.last_run_datetime,
|
self.last_run_datetime,
|
||||||
self.current_run_datetime,
|
self.current_run_datetime,
|
||||||
),
|
)
|
||||||
BookcaseItemBorrowing.delivered.is_(None),
|
|
||||||
),
|
|
||||||
).all()
|
|
||||||
for borrowing in borrowings_to_remind:
|
for borrowing in borrowings_to_remind:
|
||||||
self._send_close_deadline_mail(borrowing)
|
self._send_close_deadline_mail(borrowing)
|
||||||
|
|
||||||
def send_overdue_mails(self) -> None:
|
def send_overdue_mails(self) -> None:
|
||||||
logging.info("Sending mails for overdue items")
|
logging.info("Sending mails for overdue items")
|
||||||
|
|
||||||
to_remind = self.sql_session.scalars(
|
to_remind = list_undelivered_overdue_borrowings(
|
||||||
select(BookcaseItemBorrowing).where(
|
self.sql_session,
|
||||||
BookcaseItemBorrowing.end_time < self.current_run_datetime,
|
self.current_run_datetime,
|
||||||
BookcaseItemBorrowing.delivered.is_(None),
|
)
|
||||||
),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
for borrowing in to_remind:
|
for borrowing in to_remind:
|
||||||
self._send_overdue_mail(borrowing)
|
self._send_overdue_mail(borrowing)
|
||||||
@@ -206,24 +194,11 @@ class DeadlineDaemon:
|
|||||||
def send_newly_available_mails(self) -> None:
|
def send_newly_available_mails(self) -> None:
|
||||||
logging.info("Sending mails about newly available items")
|
logging.info("Sending mails about newly available items")
|
||||||
|
|
||||||
newly_available = self.sql_session.scalars(
|
newly_available = list_newly_available_queue_items(
|
||||||
select(BookcaseItemBorrowingQueue)
|
self.sql_session,
|
||||||
.join(
|
|
||||||
BookcaseItemBorrowing,
|
|
||||||
BookcaseItemBorrowing.fk_bookcase_item_uid
|
|
||||||
== BookcaseItemBorrowingQueue.fk_bookcase_item_uid,
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
BookcaseItemBorrowingQueue.expired.is_(False),
|
|
||||||
BookcaseItemBorrowing.delivered.is_not(None),
|
|
||||||
BookcaseItemBorrowing.delivered.between(
|
|
||||||
self.last_run_datetime,
|
self.last_run_datetime,
|
||||||
self.current_run_datetime,
|
self.current_run_datetime,
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.order_by(BookcaseItemBorrowingQueue.entered_queue_time)
|
|
||||||
.group_by(BookcaseItemBorrowingQueue.fk_bookcase_item_uid),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
for queue_item in newly_available:
|
for queue_item in newly_available:
|
||||||
logging.info(
|
logging.info(
|
||||||
@@ -243,23 +218,11 @@ class DeadlineDaemon:
|
|||||||
for d in Config["deadline_daemon.warn_days_before_expiring_queue_position_deadline"]
|
for d in Config["deadline_daemon.warn_days_before_expiring_queue_position_deadline"]
|
||||||
]
|
]
|
||||||
for day in days:
|
for day in days:
|
||||||
queue_positions_to_remind = self.sql_session.scalars(
|
queue_positions_to_remind = list_expiring_queue_positions(
|
||||||
select(BookcaseItemBorrowingQueue)
|
self.sql_session,
|
||||||
.join(
|
|
||||||
BookcaseItemBorrowing,
|
|
||||||
BookcaseItemBorrowing.fk_bookcase_item_uid
|
|
||||||
== BookcaseItemBorrowingQueue.fk_bookcase_item_uid,
|
|
||||||
)
|
|
||||||
.where(
|
|
||||||
self._sql_subtract_date(
|
|
||||||
BookcaseItemBorrowingQueue.item_became_available_time + timedelta(days=day),
|
|
||||||
timedelta(days=day),
|
|
||||||
).between(
|
|
||||||
self.last_run_datetime,
|
self.last_run_datetime,
|
||||||
self.current_run_datetime,
|
self.current_run_datetime,
|
||||||
),
|
)
|
||||||
),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
for queue_position in queue_positions_to_remind:
|
for queue_position in queue_positions_to_remind:
|
||||||
self._send_expiring_queue_position_mail(queue_position, day)
|
self._send_expiring_queue_position_mail(queue_position, day)
|
||||||
@@ -271,14 +234,11 @@ class DeadlineDaemon:
|
|||||||
Config["deadline_daemon.days_before_queue_position_expires"],
|
Config["deadline_daemon.days_before_queue_position_expires"],
|
||||||
)
|
)
|
||||||
|
|
||||||
overdue_queue_positions = self.sql_session.scalars(
|
overdue_queue_positions = list_overdue_queue_positions(
|
||||||
select(BookcaseItemBorrowingQueue).where(
|
self.sql_session,
|
||||||
BookcaseItemBorrowingQueue.item_became_available_time
|
queue_position_expiry_days,
|
||||||
+ timedelta(days=queue_position_expiry_days)
|
self.current_run_datetime,
|
||||||
< self.current_run_datetime,
|
)
|
||||||
BookcaseItemBorrowingQueue.expired.is_(False),
|
|
||||||
),
|
|
||||||
).all()
|
|
||||||
|
|
||||||
for queue_position in overdue_queue_positions:
|
for queue_position in overdue_queue_positions:
|
||||||
logging.info(
|
logging.info(
|
||||||
@@ -287,16 +247,10 @@ class DeadlineDaemon:
|
|||||||
|
|
||||||
queue_position.expired = True
|
queue_position.expired = True
|
||||||
|
|
||||||
next_queue_position = self.sql_session.scalars(
|
next_queue_position = find_next_queue_position(
|
||||||
select(BookcaseItemBorrowingQueue)
|
self.sql_session,
|
||||||
.where(
|
queue_position.fk_bookcase_item_uid,
|
||||||
BookcaseItemBorrowingQueue.fk_bookcase_item_uid
|
|
||||||
== queue_position.fk_bookcase_item_uid,
|
|
||||||
BookcaseItemBorrowingQueue.item_became_available_time.is_(None),
|
|
||||||
)
|
)
|
||||||
.order_by(BookcaseItemBorrowingQueue.entered_queue_time)
|
|
||||||
.limit(1),
|
|
||||||
).one_or_none()
|
|
||||||
|
|
||||||
self._send_queue_position_expired_mail(queue_position)
|
self._send_queue_position_expired_mail(queue_position)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
from worblehat.cli.subclis import select_bookcase_shelf
|
||||||
|
from sqlalchemy import select, event
|
||||||
|
from sqlalchemy.orm.session import Session
|
||||||
|
from worblehat.models import Bookcase, BookcaseShelf
|
||||||
|
from worblehat.services.metadata_fetchers import fetch_metadata_from_multiple_sources
|
||||||
|
from worblehat.services import is_valid_isbn
|
||||||
|
from pprint import pprint
|
||||||
|
from libdib.repl import (
|
||||||
|
NumberedCmd,
|
||||||
|
prompt_yes_no,
|
||||||
|
)
|
||||||
|
|
||||||
|
class BatchScanner(NumberedCmd):
|
||||||
|
def __init__(self, sql_session: Session):
|
||||||
|
super().__init__()
|
||||||
|
self.sql_session = sql_session
|
||||||
|
self.sql_session_dirty = False
|
||||||
|
self.bookcase_shelf = None
|
||||||
|
self.prompt_header = "[SHELF| NONE]"
|
||||||
|
|
||||||
|
@event.listens_for(self.sql_session, "after_flush")
|
||||||
|
def mark_session_as_dirty(*_):
|
||||||
|
self.sql_session_dirty = True
|
||||||
|
self.prompt_header = "(unsaved changes)"
|
||||||
|
|
||||||
|
@event.listens_for(self.sql_session, "after_commit")
|
||||||
|
@event.listens_for(self.sql_session, "after_rollback")
|
||||||
|
def mark_session_as_clean(*_):
|
||||||
|
self.sql_session_dirty = False
|
||||||
|
self.prompt_header = None
|
||||||
|
|
||||||
|
|
||||||
|
def _set_bookcase_shelf(self, bookcase_shelf: BookcaseShelf):
|
||||||
|
self.bookcase_shelf = bookcase_shelf
|
||||||
|
self.prompt_header = f"[SHELF| {bookcase_shelf.bookcase.uid} {bookcase_shelf.bookcase.description} / {bookcase_shelf.column}-{bookcase_shelf.row}: {bookcase_shelf.description}]"
|
||||||
|
|
||||||
|
|
||||||
|
def default(self, isbn: str):
|
||||||
|
isbn = isbn.strip()
|
||||||
|
if not is_valid_isbn(isbn):
|
||||||
|
super()._default(isbn)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.bookcase_shelf is None:
|
||||||
|
print("Please set the bookcase shelf first.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Scanned ISBN: {isbn}")
|
||||||
|
|
||||||
|
data = fetch_metadata_from_multiple_sources(isbn)
|
||||||
|
|
||||||
|
pprint(data)
|
||||||
|
|
||||||
|
|
||||||
|
def do_set_bookcase_shelf(self, _: str):
|
||||||
|
bookcase = self._choose_bookcase(self.sql_session)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
self._print_bookcase_shelves(
|
||||||
|
sql_session=self.sql_session,
|
||||||
|
bookcase=bookcase,
|
||||||
|
)
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
bookcase_shelf = select_bookcase_shelf(
|
||||||
|
sql_session=self.sql_session,
|
||||||
|
bookcase=bookcase,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._set_bookcase_shelf(bookcase_shelf)
|
||||||
|
|
||||||
|
print(f"Bookcase shelf set to {bookcase_shelf}")
|
||||||
|
|
||||||
|
def _choose_bookcase(
|
||||||
|
self,
|
||||||
|
sql_session: Session,
|
||||||
|
) -> Bookcase:
|
||||||
|
bookcases = sql_session.scalars(
|
||||||
|
select(Bookcase)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
print("Available bookcases:")
|
||||||
|
for bookcase in bookcases:
|
||||||
|
print(f" {bookcase.name} - {bookcase.description}")
|
||||||
|
|
||||||
|
bookcase_name = input("Choose a bookcase> ").strip()
|
||||||
|
|
||||||
|
bookcase = sql_session.scalars(
|
||||||
|
select(Bookcase).where(Bookcase.name == bookcase_name)
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
if not bookcase:
|
||||||
|
print(f"Bookcase {bookcase_name} not found")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return bookcase
|
||||||
|
|
||||||
|
def _print_bookcase_shelves(
|
||||||
|
self,
|
||||||
|
sql_session: Session,
|
||||||
|
bookcase: Bookcase,
|
||||||
|
) -> None:
|
||||||
|
shelves = sql_session.scalars(
|
||||||
|
select(BookcaseShelf).where(BookcaseShelf.bookcase == bookcase)
|
||||||
|
).all()
|
||||||
|
min_col = min([shelf.column for shelf in shelves])
|
||||||
|
max_col = max([shelf.column for shelf in shelves])
|
||||||
|
min_row = min([shelf.row for shelf in shelves])
|
||||||
|
max_row = max([shelf.row for shelf in shelves])
|
||||||
|
|
||||||
|
print('Available shelves:')
|
||||||
|
for col in range(min_col, max_col + 1):
|
||||||
|
for row in range(min_row, max_row + 1):
|
||||||
|
shelf = sql_session.scalars(
|
||||||
|
select(BookcaseShelf).where(
|
||||||
|
BookcaseShelf.bookcase == bookcase,
|
||||||
|
BookcaseShelf.column == col,
|
||||||
|
BookcaseShelf.row == row,
|
||||||
|
)
|
||||||
|
).one_or_none()
|
||||||
|
if shelf:
|
||||||
|
print(f"{col}-{row}: {shelf.description}")
|
||||||
|
else:
|
||||||
|
print(f"{col}-{row}: None")
|
||||||
|
|
||||||
|
def do_save(self, _: str):
|
||||||
|
if not self.sql_session_dirty:
|
||||||
|
print("No changes to save.")
|
||||||
|
return
|
||||||
|
self.sql_session.commit()
|
||||||
|
|
||||||
|
def do_abort(self, _: str):
|
||||||
|
if not self.sql_session_dirty:
|
||||||
|
print("No changes to abort.")
|
||||||
|
return
|
||||||
|
self.sql_session.rollback()
|
||||||
|
|
||||||
|
def do_exit(self, _: str):
|
||||||
|
if self.sql_session_dirty:
|
||||||
|
if prompt_yes_no("Would you like to save your changes?"):
|
||||||
|
self.sql_session.commit()
|
||||||
|
else:
|
||||||
|
self.sql_session.rollback()
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
funcs = {
|
||||||
|
0: {
|
||||||
|
"f": default,
|
||||||
|
"doc": "Add item with its ISBN",
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
'f' : do_set_bookcase_shelf,
|
||||||
|
'doc' : 'Select shelf',
|
||||||
|
},
|
||||||
|
7: {
|
||||||
|
"f": do_save,
|
||||||
|
"doc": "Save changes",
|
||||||
|
},
|
||||||
|
8: {
|
||||||
|
"f": do_abort,
|
||||||
|
"doc": "Abort changes",
|
||||||
|
},
|
||||||
|
9: {
|
||||||
|
"f": do_exit,
|
||||||
|
"doc": "Exit",
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -86,6 +86,12 @@ def main() -> None:
|
|||||||
from .devscripts.seed_test_data import main
|
from .devscripts.seed_test_data import main
|
||||||
|
|
||||||
main(sql_session)
|
main(sql_session)
|
||||||
|
elif args.script == "batch-scanner":
|
||||||
|
from .devscripts.batch_scanner import BatchScanner
|
||||||
|
|
||||||
|
result = BatchScanner(sql_session).cmdloop()
|
||||||
|
|
||||||
|
print(result)
|
||||||
else:
|
else:
|
||||||
print(devscripts_arg_parser.format_help())
|
print(devscripts_arg_parser.format_help())
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from .author import search_authors_by_name
|
||||||
|
from .bookcase import find_bookcase_by_name
|
||||||
|
from .bookcase_item import (
|
||||||
|
find_bookcase_item_by_isbn,
|
||||||
|
find_bookcase_item_by_name,
|
||||||
|
list_bookcase_items_by_owner,
|
||||||
|
search_bookcase_item_owners,
|
||||||
|
search_bookcase_items_by_title,
|
||||||
|
)
|
||||||
|
from .bookcase_shelf import (
|
||||||
|
find_bookcase_shelf,
|
||||||
|
list_bookcase_shelf_positions,
|
||||||
|
list_bookcase_shelfs_ordered,
|
||||||
|
)
|
||||||
|
from .borrowing import (
|
||||||
|
has_active_borrowing,
|
||||||
|
list_active_borrowings,
|
||||||
|
list_active_borrowings_for_item,
|
||||||
|
list_borrowings_for_isbn,
|
||||||
|
list_overdue_borrowings,
|
||||||
|
)
|
||||||
|
from .borrowing_queue import (
|
||||||
|
is_in_borrowing_queue,
|
||||||
|
list_all_queue_items,
|
||||||
|
list_pending_queue_items_for_item,
|
||||||
|
)
|
||||||
|
from .deadline_daemon import (
|
||||||
|
find_last_run,
|
||||||
|
find_next_queue_position,
|
||||||
|
list_close_deadline_borrowings,
|
||||||
|
list_expiring_queue_positions,
|
||||||
|
list_newly_available_queue_items,
|
||||||
|
list_overdue_queue_positions,
|
||||||
|
list_undelivered_overdue_borrowings,
|
||||||
|
)
|
||||||
|
from .media_type import find_media_type_by_name
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"find_bookcase_by_name",
|
||||||
|
"find_bookcase_item_by_isbn",
|
||||||
|
"find_bookcase_item_by_name",
|
||||||
|
"find_bookcase_shelf",
|
||||||
|
"find_last_run",
|
||||||
|
"find_media_type_by_name",
|
||||||
|
"find_next_queue_position",
|
||||||
|
"has_active_borrowing",
|
||||||
|
"is_in_borrowing_queue",
|
||||||
|
"list_active_borrowings",
|
||||||
|
"list_active_borrowings_for_item",
|
||||||
|
"list_all_queue_items",
|
||||||
|
"list_bookcase_items_by_owner",
|
||||||
|
"list_bookcase_shelf_positions",
|
||||||
|
"list_bookcase_shelfs_ordered",
|
||||||
|
"list_borrowings_for_isbn",
|
||||||
|
"list_close_deadline_borrowings",
|
||||||
|
"list_expiring_queue_positions",
|
||||||
|
"list_newly_available_queue_items",
|
||||||
|
"list_overdue_borrowings",
|
||||||
|
"list_overdue_queue_positions",
|
||||||
|
"list_pending_queue_items_for_item",
|
||||||
|
"list_undelivered_overdue_borrowings",
|
||||||
|
"search_authors_by_name",
|
||||||
|
"search_bookcase_item_owners",
|
||||||
|
"search_bookcase_items_by_title",
|
||||||
|
]
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import Author
|
||||||
|
|
||||||
|
|
||||||
|
def search_authors_by_name(sql_session: Session, text: str) -> list[Author]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(Author).where(Author.name.ilike(f"%{text}%")),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import Bookcase
|
||||||
|
|
||||||
|
|
||||||
|
def find_bookcase_by_name(sql_session: Session, name: str) -> Bookcase | None:
|
||||||
|
return sql_session.scalars(
|
||||||
|
select(Bookcase).where(Bookcase.name == name),
|
||||||
|
).one_or_none()
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import BookcaseItem
|
||||||
|
|
||||||
|
|
||||||
|
def find_bookcase_item_by_isbn(sql_session: Session, isbn: str) -> BookcaseItem | None:
|
||||||
|
return sql_session.scalars(
|
||||||
|
select(BookcaseItem).where(BookcaseItem.isbn == isbn),
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
def find_bookcase_item_by_name(sql_session: Session, name: str) -> BookcaseItem | None:
|
||||||
|
return sql_session.scalars(
|
||||||
|
select(BookcaseItem).where(BookcaseItem.name == name),
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
def search_bookcase_items_by_title(sql_session: Session, text: str) -> list[BookcaseItem]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItem).where(BookcaseItem.name.ilike(f"%{text}%")),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def search_bookcase_item_owners(sql_session: Session, text: str) -> list[str]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItem.owner)
|
||||||
|
.where(BookcaseItem.owner.ilike(f"%{text}%"))
|
||||||
|
.distinct(),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_bookcase_items_by_owner(sql_session: Session, owner: str) -> list[BookcaseItem]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItem).where(BookcaseItem.owner == owner),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import Bookcase, BookcaseShelf
|
||||||
|
|
||||||
|
|
||||||
|
def find_bookcase_shelf(
|
||||||
|
sql_session: Session,
|
||||||
|
bookcase: Bookcase,
|
||||||
|
column: int,
|
||||||
|
row: int,
|
||||||
|
) -> BookcaseShelf | None:
|
||||||
|
return sql_session.scalars(
|
||||||
|
select(BookcaseShelf).where(
|
||||||
|
BookcaseShelf.bookcase == bookcase,
|
||||||
|
BookcaseShelf.column == column,
|
||||||
|
BookcaseShelf.row == row,
|
||||||
|
),
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
def list_bookcase_shelfs_ordered(sql_session: Session) -> list[BookcaseShelf]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseShelf)
|
||||||
|
.join(Bookcase)
|
||||||
|
.order_by(
|
||||||
|
Bookcase.name,
|
||||||
|
BookcaseShelf.column,
|
||||||
|
BookcaseShelf.row,
|
||||||
|
),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_bookcase_shelf_positions(
|
||||||
|
sql_session: Session,
|
||||||
|
bookcase: Bookcase,
|
||||||
|
column: int | None = None,
|
||||||
|
row: int | None = None,
|
||||||
|
) -> list[tuple[int, int]]:
|
||||||
|
query = select(BookcaseShelf.row, BookcaseShelf.column).where(
|
||||||
|
BookcaseShelf.bookcase == bookcase,
|
||||||
|
)
|
||||||
|
if column is not None:
|
||||||
|
query = query.where(BookcaseShelf.column == column)
|
||||||
|
if row is not None:
|
||||||
|
query = query.where(BookcaseShelf.row == row)
|
||||||
|
return [tuple(r) for r in sql_session.execute(query)]
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import BookcaseItem, BookcaseItemBorrowing
|
||||||
|
|
||||||
|
|
||||||
|
def list_active_borrowings(sql_session: Session) -> list[BookcaseItemBorrowing]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowing)
|
||||||
|
.where(BookcaseItemBorrowing.delivered.is_(None))
|
||||||
|
.order_by(BookcaseItemBorrowing.end_time),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_active_borrowings_for_item(
|
||||||
|
sql_session: Session,
|
||||||
|
item: BookcaseItem,
|
||||||
|
) -> list[BookcaseItemBorrowing]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowing)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowing.item == item,
|
||||||
|
BookcaseItemBorrowing.delivered.is_(None),
|
||||||
|
)
|
||||||
|
.order_by(BookcaseItemBorrowing.end_time),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_active_borrowing(sql_session: Session, username: str, item: BookcaseItem) -> bool:
|
||||||
|
return (
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowing).where(
|
||||||
|
BookcaseItemBorrowing.username == username,
|
||||||
|
BookcaseItemBorrowing.item == item,
|
||||||
|
BookcaseItemBorrowing.delivered.is_(None),
|
||||||
|
),
|
||||||
|
).one_or_none()
|
||||||
|
is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_borrowings_for_isbn(sql_session: Session, isbn: str) -> list[BookcaseItemBorrowing]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowing)
|
||||||
|
.join(
|
||||||
|
BookcaseItem,
|
||||||
|
BookcaseItem.uid == BookcaseItemBorrowing.fk_bookcase_item_uid,
|
||||||
|
)
|
||||||
|
.where(BookcaseItem.isbn == isbn)
|
||||||
|
.order_by(BookcaseItemBorrowing.username),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_overdue_borrowings(sql_session: Session) -> list[BookcaseItemBorrowing]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowing)
|
||||||
|
.join(BookcaseItem)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowing.end_time < datetime.now(),
|
||||||
|
BookcaseItemBorrowing.delivered.is_(None),
|
||||||
|
)
|
||||||
|
.order_by(BookcaseItemBorrowing.end_time),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import BookcaseItem, BookcaseItemBorrowingQueue
|
||||||
|
|
||||||
|
|
||||||
|
def list_all_queue_items(sql_session: Session) -> list[BookcaseItemBorrowingQueue]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowingQueue).order_by(
|
||||||
|
BookcaseItemBorrowingQueue.entered_queue_time,
|
||||||
|
),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_in_borrowing_queue(sql_session: Session, username: str, item: BookcaseItem) -> bool:
|
||||||
|
return (
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowingQueue).where(
|
||||||
|
BookcaseItemBorrowingQueue.username == username,
|
||||||
|
BookcaseItemBorrowingQueue.item == item,
|
||||||
|
),
|
||||||
|
).one_or_none()
|
||||||
|
is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_pending_queue_items_for_item(
|
||||||
|
sql_session: Session,
|
||||||
|
item: BookcaseItem,
|
||||||
|
) -> list[BookcaseItemBorrowingQueue]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowingQueue)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowingQueue.item == item,
|
||||||
|
BookcaseItemBorrowingQueue.item_became_available_time.is_(None),
|
||||||
|
)
|
||||||
|
.order_by(BookcaseItemBorrowingQueue.entered_queue_time),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from sqlalchemy import func, select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy.sql.elements import SQLColumnExpression
|
||||||
|
|
||||||
|
from worblehat.models import (
|
||||||
|
BookcaseItemBorrowing,
|
||||||
|
BookcaseItemBorrowingQueue,
|
||||||
|
DeadlineDaemonLastRunDatetime,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _sql_subtract_date(
|
||||||
|
sql_session: Session,
|
||||||
|
x: SQLColumnExpression[datetime],
|
||||||
|
y: timedelta,
|
||||||
|
) -> SQLColumnExpression[datetime]:
|
||||||
|
dialect_name = sql_session.get_bind().dialect.name
|
||||||
|
if dialect_name == "sqlite":
|
||||||
|
# SQLite does not support timedelta in queries
|
||||||
|
return func.datetime(x, f"-{y.days} days")
|
||||||
|
if dialect_name == "postgresql":
|
||||||
|
return x - y
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"Unsupported dialect: {dialect_name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_last_run(sql_session: Session) -> DeadlineDaemonLastRunDatetime | None:
|
||||||
|
return sql_session.scalars(
|
||||||
|
select(DeadlineDaemonLastRunDatetime),
|
||||||
|
).one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
def list_close_deadline_borrowings(
|
||||||
|
sql_session: Session,
|
||||||
|
day: int,
|
||||||
|
last_run_datetime: datetime,
|
||||||
|
current_run_datetime: datetime,
|
||||||
|
) -> list[BookcaseItemBorrowing]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowing).where(
|
||||||
|
_sql_subtract_date(
|
||||||
|
sql_session,
|
||||||
|
BookcaseItemBorrowing.end_time,
|
||||||
|
timedelta(days=day),
|
||||||
|
).between(
|
||||||
|
last_run_datetime,
|
||||||
|
current_run_datetime,
|
||||||
|
),
|
||||||
|
BookcaseItemBorrowing.delivered.is_(None),
|
||||||
|
),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_undelivered_overdue_borrowings(
|
||||||
|
sql_session: Session,
|
||||||
|
current_run_datetime: datetime,
|
||||||
|
) -> list[BookcaseItemBorrowing]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowing).where(
|
||||||
|
BookcaseItemBorrowing.end_time < current_run_datetime,
|
||||||
|
BookcaseItemBorrowing.delivered.is_(None),
|
||||||
|
),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_newly_available_queue_items(
|
||||||
|
sql_session: Session,
|
||||||
|
last_run_datetime: datetime,
|
||||||
|
current_run_datetime: datetime,
|
||||||
|
) -> list[BookcaseItemBorrowingQueue]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowingQueue)
|
||||||
|
.join(
|
||||||
|
BookcaseItemBorrowing,
|
||||||
|
BookcaseItemBorrowing.fk_bookcase_item_uid
|
||||||
|
== BookcaseItemBorrowingQueue.fk_bookcase_item_uid,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowingQueue.expired.is_(False),
|
||||||
|
BookcaseItemBorrowing.delivered.is_not(None),
|
||||||
|
BookcaseItemBorrowing.delivered.between(
|
||||||
|
last_run_datetime,
|
||||||
|
current_run_datetime,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.order_by(BookcaseItemBorrowingQueue.entered_queue_time)
|
||||||
|
.group_by(BookcaseItemBorrowingQueue.fk_bookcase_item_uid),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_expiring_queue_positions(
|
||||||
|
sql_session: Session,
|
||||||
|
last_run_datetime: datetime,
|
||||||
|
current_run_datetime: datetime,
|
||||||
|
) -> list[BookcaseItemBorrowingQueue]:
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowingQueue)
|
||||||
|
.join(
|
||||||
|
BookcaseItemBorrowing,
|
||||||
|
BookcaseItemBorrowing.fk_bookcase_item_uid
|
||||||
|
== BookcaseItemBorrowingQueue.fk_bookcase_item_uid,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowingQueue.item_became_available_time.between(
|
||||||
|
last_run_datetime,
|
||||||
|
current_run_datetime,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_overdue_queue_positions(
|
||||||
|
sql_session: Session,
|
||||||
|
queue_position_expiry_days: int,
|
||||||
|
current_run_datetime: datetime,
|
||||||
|
) -> list[BookcaseItemBorrowingQueue]:
|
||||||
|
expiry_cutoff = current_run_datetime - timedelta(days=queue_position_expiry_days)
|
||||||
|
return list(
|
||||||
|
sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowingQueue).where(
|
||||||
|
BookcaseItemBorrowingQueue.item_became_available_time < expiry_cutoff,
|
||||||
|
BookcaseItemBorrowingQueue.expired.is_(False),
|
||||||
|
),
|
||||||
|
).all(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def find_next_queue_position(
|
||||||
|
sql_session: Session,
|
||||||
|
item_uid: int,
|
||||||
|
) -> BookcaseItemBorrowingQueue | None:
|
||||||
|
return sql_session.scalars(
|
||||||
|
select(BookcaseItemBorrowingQueue)
|
||||||
|
.where(
|
||||||
|
BookcaseItemBorrowingQueue.fk_bookcase_item_uid == item_uid,
|
||||||
|
BookcaseItemBorrowingQueue.item_became_available_time.is_(None),
|
||||||
|
)
|
||||||
|
.order_by(BookcaseItemBorrowingQueue.entered_queue_time)
|
||||||
|
.limit(1),
|
||||||
|
).one_or_none()
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import MediaType
|
||||||
|
|
||||||
|
|
||||||
|
def find_media_type_by_name(sql_session: Session, name: str) -> MediaType:
|
||||||
|
return sql_session.scalars(
|
||||||
|
select(MediaType).where(MediaType.name.ilike(name)),
|
||||||
|
).one()
|
||||||
@@ -52,6 +52,11 @@ devscripts_subparsers.add_parser(
|
|||||||
help="Seed data tailorded for testing the deadline daemon, into the database",
|
help="Seed data tailorded for testing the deadline daemon, into the database",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
devscripts_subparsers.add_parser(
|
||||||
|
"batch-scanner",
|
||||||
|
help="REPL optimized for scanning in tons of books",
|
||||||
|
)
|
||||||
|
|
||||||
arg_parser.add_argument(
|
arg_parser.add_argument(
|
||||||
"-V",
|
"-V",
|
||||||
"--version",
|
"--version",
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import isbnlib
|
|||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.book_data_fetchers import fetch_book_data_from_multiple_sources
|
||||||
|
|
||||||
from ..models import (
|
from ..models import (
|
||||||
Author,
|
Author,
|
||||||
BookcaseItem,
|
BookcaseItem,
|
||||||
Language,
|
Language,
|
||||||
)
|
)
|
||||||
from .metadata_fetchers import fetch_metadata_from_multiple_sources
|
|
||||||
|
|
||||||
|
|
||||||
def is_valid_pvv_isbn(isbn: str) -> bool:
|
def is_valid_pvv_isbn(isbn: str) -> bool:
|
||||||
@@ -41,7 +42,7 @@ def create_bookcase_item_from_isbn(
|
|||||||
Please not that the returned BookcaseItem will likely not be fully populated with the required
|
Please not that the returned BookcaseItem will likely not be fully populated with the required
|
||||||
data, such as the book's location in the library, and the owner of the book, etc.
|
data, such as the book's location in the library, and the owner of the book, etc.
|
||||||
"""
|
"""
|
||||||
metadata = fetch_metadata_from_multiple_sources(isbn)
|
metadata = fetch_book_data_from_multiple_sources(isbn)
|
||||||
if len(metadata) == 0:
|
if len(metadata) == 0:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
# base fetcher.
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
from .BookMetadata import BookMetadata
|
|
||||||
|
|
||||||
|
|
||||||
class BookMetadataFetcher(ABC):
|
|
||||||
"""
|
|
||||||
A base class for metadata fetchers.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@abstractmethod
|
|
||||||
def metadata_source_id(cls) -> str:
|
|
||||||
"""Returns a unique identifier for the metadata source, to identify where the metadata came from."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@abstractmethod
|
|
||||||
def fetch_metadata(cls, isbn: str) -> BookMetadata | None:
|
|
||||||
"""Tries to fetch metadata for the given ISBN."""
|
|
||||||
pass
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from .book_metadata_fetcher import fetch_metadata_from_multiple_sources
|
|
||||||
|
|
||||||
__all__ = ["fetch_metadata_from_multiple_sources"]
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
"""
|
|
||||||
this module contains the fetch_book_metadata() function which fetches book metadata from multiple sources in threads and returns the higest ranked non-None result.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
|
||||||
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadata import BookMetadata
|
|
||||||
from worblehat.services.metadata_fetchers.BookMetadataFetcher import BookMetadataFetcher
|
|
||||||
from worblehat.services.metadata_fetchers.GoogleBooksFetcher import GoogleBooksFetcher
|
|
||||||
from worblehat.services.metadata_fetchers.OpenLibraryFetcher import OpenLibraryFetcher
|
|
||||||
from worblehat.services.metadata_fetchers.OutlandScraperFetcher import (
|
|
||||||
OutlandScraperFetcher,
|
|
||||||
)
|
|
||||||
|
|
||||||
# The order of these fetchers determines the priority of the sources.
|
|
||||||
# The first fetcher in the list has the highest priority.
|
|
||||||
FETCHERS: list[BookMetadataFetcher] = [
|
|
||||||
OpenLibraryFetcher,
|
|
||||||
GoogleBooksFetcher,
|
|
||||||
OutlandScraperFetcher,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
FETCHER_SOURCE_IDS: list[str] = [fetcher.metadata_source_id() for fetcher in FETCHERS]
|
|
||||||
|
|
||||||
|
|
||||||
def sort_metadata_by_priority(metadata: list[BookMetadata]) -> list[BookMetadata]:
|
|
||||||
"""
|
|
||||||
Sorts the given metadata by the priority of the sources.
|
|
||||||
|
|
||||||
The order of the metadata is the same as the order of the sources in the FETCHERS list.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Note that this function is O(n^2) but the number of fetchers is small so it's fine.
|
|
||||||
return sorted(metadata, key=lambda m: FETCHER_SOURCE_IDS.index(m.source))
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_metadata_from_multiple_sources(isbn: str, strict: bool=False) -> list[BookMetadata]:
|
|
||||||
"""
|
|
||||||
Returns a list of metadata fetched from multiple sources.
|
|
||||||
|
|
||||||
Sources that does not have metadata for the given ISBN will be ignored.
|
|
||||||
|
|
||||||
There is no guarantee that there will be any metadata.
|
|
||||||
|
|
||||||
The results are always ordered in the same way as the fetchers are listed in the FETCHERS list.
|
|
||||||
"""
|
|
||||||
isbn = isbn.replace("-", "").replace("_", "").strip().lower()
|
|
||||||
if len(isbn) != 10 and len(isbn) != 13 and not isbn.isnumeric():
|
|
||||||
raise ValueError("Invalid ISBN")
|
|
||||||
|
|
||||||
results: list[BookMetadata] = []
|
|
||||||
|
|
||||||
with ThreadPoolExecutor() as executor:
|
|
||||||
futures = [executor.submit(fetcher.fetch_metadata, isbn) for fetcher in FETCHERS]
|
|
||||||
|
|
||||||
for future in futures:
|
|
||||||
result = future.result()
|
|
||||||
if result is not None:
|
|
||||||
results.append(result)
|
|
||||||
|
|
||||||
for result in results:
|
|
||||||
try:
|
|
||||||
result.validate()
|
|
||||||
except ValueError as e:
|
|
||||||
if strict:
|
|
||||||
raise e
|
|
||||||
print(f"Invalid metadata: {e}")
|
|
||||||
results.remove(result)
|
|
||||||
|
|
||||||
return sort_metadata_by_priority(results)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
from pprint import pprint
|
|
||||||
|
|
||||||
isbn = "0132624788"
|
|
||||||
metadata = fetch_metadata_from_multiple_sources(isbn)
|
|
||||||
pprint(metadata)
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import sqlparse
|
||||||
|
from sqlalchemy import create_engine, event
|
||||||
|
|
||||||
|
# from sqlalchemy.exc import OperationalError
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import Base
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption(
|
||||||
|
"--debug-sql",
|
||||||
|
action="store_true",
|
||||||
|
help="Enable SQLAlchemy 'echo' mode for debugging",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SqlParseFormatter(logging.Formatter):
|
||||||
|
def format(self, record):
|
||||||
|
recordMessage = record.getMessage()
|
||||||
|
if not recordMessage.startswith("[") and any(
|
||||||
|
recordMessage.startswith(keyword)
|
||||||
|
for keyword in [
|
||||||
|
"SELECT",
|
||||||
|
"INSERT",
|
||||||
|
"UPDATE",
|
||||||
|
"DELETE",
|
||||||
|
"WITH",
|
||||||
|
]
|
||||||
|
):
|
||||||
|
formatted_sql = sqlparse.format(recordMessage, reindent=True, keyword_case="upper")
|
||||||
|
record.msg = "\n" + formatted_sql
|
||||||
|
|
||||||
|
return super().format(record)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_configure(config):
|
||||||
|
"""Setup pretty SQL logging if --debug-sql is enabled."""
|
||||||
|
|
||||||
|
logger = logging.getLogger("sqlalchemy.engine")
|
||||||
|
|
||||||
|
# TODO: it would be nice not to duplicate these logs.
|
||||||
|
# logging.NullHandler() does not seem to work here
|
||||||
|
handler = logging.StreamHandler()
|
||||||
|
handler.setFormatter(SqlParseFormatter())
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
|
if config.getoption("--debug-sql"):
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def sql_session(request):
|
||||||
|
"""Create a new SQLAlchemy session for testing."""
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///:memory:")
|
||||||
|
|
||||||
|
@event.listens_for(engine, "connect")
|
||||||
|
def set_sqlite_pragma(dbapi_connection, _connection_record):
|
||||||
|
cursor = dbapi_connection.cursor()
|
||||||
|
cursor.execute("PRAGMA foreign_keys=ON")
|
||||||
|
cursor.close()
|
||||||
|
|
||||||
|
Base.metadata.create_all(engine)
|
||||||
|
with Session(engine) as sql_session:
|
||||||
|
yield sql_session
|
||||||
|
sql_session.close()
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME: Declaring this hook seems to have a side effect where the database does not
|
||||||
|
# get reset between tests.
|
||||||
|
# @pytest.hookimpl(trylast=True)
|
||||||
|
# def pytest_runtest_call(item: pytest.Item):
|
||||||
|
# """Hook to format SQL statements in OperationalError exceptions."""
|
||||||
|
# try:
|
||||||
|
# item.runtest()
|
||||||
|
# except OperationalError as e:
|
||||||
|
# if e.statement is not None:
|
||||||
|
# formatted_sql = sqlparse.format(e.statement, reindent=True, keyword_case="upper")
|
||||||
|
# e.statement = "\n" + formatted_sql + "\n"
|
||||||
|
# raise e
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import Author
|
||||||
|
from worblehat.queries.author import search_authors_by_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_authors_by_name_matches_substring_case_insensitively(sql_session: Session) -> None:
|
||||||
|
tolkien = Author(name="J.R.R. Tolkien")
|
||||||
|
orwell = Author(name="George Orwell")
|
||||||
|
sql_session.add_all([tolkien, orwell])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = search_authors_by_name(sql_session, "tolkien")
|
||||||
|
|
||||||
|
assert result == [tolkien]
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_authors_by_name_returns_empty_when_no_match(sql_session: Session) -> None:
|
||||||
|
sql_session.add(Author(name="Roald Dahl"))
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = search_authors_by_name(sql_session, "nonexistent")
|
||||||
|
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_authors_by_name_returns_all_matches(sql_session: Session) -> None:
|
||||||
|
tolkien = Author(name="J.R.R. Tolkien")
|
||||||
|
orwell = Author(name="George Orwell")
|
||||||
|
dahl = Author(name="Roald Dahl")
|
||||||
|
nesbo = Author(name="Jo Nesbø")
|
||||||
|
nakamura = Author(name="中村 文則")
|
||||||
|
sql_session.add_all([tolkien, orwell, dahl, nesbo, nakamura])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = search_authors_by_name(sql_session, "e")
|
||||||
|
|
||||||
|
assert set(result) == {tolkien, orwell, nesbo}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import Bookcase
|
||||||
|
from worblehat.queries.bookcase import find_bookcase_by_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_bookcase_by_name_returns_match(sql_session: Session) -> None:
|
||||||
|
bookcase = Bookcase(name="Bookcase A", description="A bookcase")
|
||||||
|
sql_session.add(bookcase)
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = find_bookcase_by_name(sql_session, "Bookcase A")
|
||||||
|
|
||||||
|
assert result is bookcase
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_bookcase_by_name_is_case_sensitive(sql_session: Session) -> None:
|
||||||
|
sql_session.add(Bookcase(name="Bookcase A"))
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = find_bookcase_by_name(sql_session, "bookcase a")
|
||||||
|
|
||||||
|
assert result is None
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import (
|
||||||
|
Bookcase,
|
||||||
|
BookcaseItem,
|
||||||
|
BookcaseShelf,
|
||||||
|
MediaType,
|
||||||
|
)
|
||||||
|
from worblehat.queries.bookcase_item import (
|
||||||
|
find_bookcase_item_by_isbn,
|
||||||
|
find_bookcase_item_by_name,
|
||||||
|
list_bookcase_items_by_owner,
|
||||||
|
search_bookcase_item_owners,
|
||||||
|
search_bookcase_items_by_title,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_bookcase_item(
|
||||||
|
sql_session: Session,
|
||||||
|
name: str = "Some Book",
|
||||||
|
isbn: str = "1234567890",
|
||||||
|
owner: str = "PVV",
|
||||||
|
) -> BookcaseItem:
|
||||||
|
media_type = MediaType(name=f"Media type for {name}")
|
||||||
|
bookcase = Bookcase(name=f"Bookcase for {name}")
|
||||||
|
shelf = BookcaseShelf(row=0, column=0, bookcase=bookcase)
|
||||||
|
sql_session.add_all([media_type, bookcase, shelf])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
item = BookcaseItem(name, isbn, owner)
|
||||||
|
item.media_type = media_type
|
||||||
|
item.shelf = shelf
|
||||||
|
sql_session.add(item)
|
||||||
|
sql_session.flush()
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_bookcase_item_by_isbn_returns_match(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session, isbn="1111111111")
|
||||||
|
|
||||||
|
result = find_bookcase_item_by_isbn(sql_session, "1111111111")
|
||||||
|
|
||||||
|
assert result is item
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_bookcase_item_by_isbn_returns_none_when_missing(sql_session: Session) -> None:
|
||||||
|
_make_bookcase_item(sql_session, isbn="1111111111")
|
||||||
|
|
||||||
|
result = find_bookcase_item_by_isbn(sql_session, "2222222222")
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_bookcase_item_by_name_returns_match(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session, name="Unique Title")
|
||||||
|
|
||||||
|
result = find_bookcase_item_by_name(sql_session, "Unique Title")
|
||||||
|
|
||||||
|
assert result is item
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_bookcase_item_by_name_returns_none_when_missing(sql_session: Session) -> None:
|
||||||
|
_make_bookcase_item(sql_session, name="Unique Title")
|
||||||
|
|
||||||
|
result = find_bookcase_item_by_name(sql_session, "Other Title")
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_bookcase_items_by_title_matches_substring_case_insensitively(
|
||||||
|
sql_session: Session,
|
||||||
|
) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session, name="The Great Gatsby", isbn="1111111111")
|
||||||
|
_make_bookcase_item(sql_session, name="Moby Dick", isbn="2222222222")
|
||||||
|
|
||||||
|
result = search_bookcase_items_by_title(sql_session, "great gatsby")
|
||||||
|
|
||||||
|
assert result == [item]
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_bookcase_items_by_title_returns_empty_when_no_match(sql_session: Session) -> None:
|
||||||
|
_make_bookcase_item(sql_session, name="The Great Gatsby")
|
||||||
|
|
||||||
|
result = search_bookcase_items_by_title(sql_session, "nonexistent")
|
||||||
|
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_bookcase_item_owners_matches_substring_case_insensitively(
|
||||||
|
sql_session: Session,
|
||||||
|
) -> None:
|
||||||
|
_make_bookcase_item(sql_session, name="Book A", isbn="1111111111", owner="alice")
|
||||||
|
_make_bookcase_item(sql_session, name="Book B", isbn="2222222222", owner="bob")
|
||||||
|
|
||||||
|
result = search_bookcase_item_owners(sql_session, "ALI")
|
||||||
|
|
||||||
|
assert result == ["alice"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_search_bookcase_item_owners_returns_distinct_owners(sql_session: Session) -> None:
|
||||||
|
_make_bookcase_item(sql_session, name="Book A", isbn="1111111111", owner="alice")
|
||||||
|
_make_bookcase_item(sql_session, name="Book B", isbn="2222222222", owner="alice")
|
||||||
|
|
||||||
|
result = search_bookcase_item_owners(sql_session, "ali")
|
||||||
|
|
||||||
|
assert result == ["alice"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_bookcase_items_by_owner_returns_only_matching_owner(sql_session: Session) -> None:
|
||||||
|
item_a = _make_bookcase_item(sql_session, name="Book A", isbn="1111111111", owner="alice")
|
||||||
|
_make_bookcase_item(sql_session, name="Book B", isbn="2222222222", owner="bob")
|
||||||
|
|
||||||
|
result = list_bookcase_items_by_owner(sql_session, "alice")
|
||||||
|
|
||||||
|
assert result == [item_a]
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import Bookcase, BookcaseShelf
|
||||||
|
from worblehat.queries.bookcase_shelf import (
|
||||||
|
find_bookcase_shelf,
|
||||||
|
list_bookcase_shelf_positions,
|
||||||
|
list_bookcase_shelfs_ordered,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_bookcase_shelf_returns_match(sql_session: Session) -> None:
|
||||||
|
bookcase = Bookcase(name="Bookcase A")
|
||||||
|
shelf = BookcaseShelf(row=0, column=0, bookcase=bookcase)
|
||||||
|
sql_session.add_all([bookcase, shelf])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = find_bookcase_shelf(sql_session, bookcase, column=0, row=0)
|
||||||
|
|
||||||
|
assert result is shelf
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_bookcase_shelf_returns_none_for_wrong_position(sql_session: Session) -> None:
|
||||||
|
bookcase = Bookcase(name="Bookcase A")
|
||||||
|
shelf = BookcaseShelf(row=0, column=0, bookcase=bookcase)
|
||||||
|
sql_session.add_all([bookcase, shelf])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
assert find_bookcase_shelf(sql_session, bookcase, column=1, row=0) is None
|
||||||
|
assert find_bookcase_shelf(sql_session, bookcase, column=0, row=1) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_bookcase_shelf_scopes_to_bookcase(sql_session: Session) -> None:
|
||||||
|
bookcase_a = Bookcase(name="Bookcase A")
|
||||||
|
bookcase_b = Bookcase(name="Bookcase B")
|
||||||
|
shelf_a = BookcaseShelf(row=0, column=0, bookcase=bookcase_a)
|
||||||
|
sql_session.add_all([bookcase_a, bookcase_b, shelf_a])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
assert find_bookcase_shelf(sql_session, bookcase_b, column=0, row=0) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_bookcase_shelfs_ordered(sql_session: Session) -> None:
|
||||||
|
bookcase_b = Bookcase(name="Bookcase B")
|
||||||
|
bookcase_a = Bookcase(name="Bookcase A")
|
||||||
|
sql_session.add_all([bookcase_b, bookcase_a])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
shelf_b1 = BookcaseShelf(row=1, column=0, bookcase=bookcase_b)
|
||||||
|
shelf_a2 = BookcaseShelf(row=0, column=2, bookcase=bookcase_a)
|
||||||
|
shelf_a1 = BookcaseShelf(row=0, column=1, bookcase=bookcase_a)
|
||||||
|
sql_session.add_all([shelf_b1, shelf_a2, shelf_a1])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_bookcase_shelfs_ordered(sql_session)
|
||||||
|
|
||||||
|
assert result == [shelf_a1, shelf_a2, shelf_b1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_bookcase_shelf_positions_returns_all_by_default(sql_session: Session) -> None:
|
||||||
|
bookcase = Bookcase(name="Bookcase A")
|
||||||
|
shelf_1 = BookcaseShelf(row=0, column=0, bookcase=bookcase)
|
||||||
|
shelf_2 = BookcaseShelf(row=1, column=1, bookcase=bookcase)
|
||||||
|
sql_session.add_all([bookcase, shelf_1, shelf_2])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_bookcase_shelf_positions(sql_session, bookcase)
|
||||||
|
|
||||||
|
assert set(result) == {(0, 0), (1, 1)}
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_bookcase_shelf_positions_filters_by_column(sql_session: Session) -> None:
|
||||||
|
bookcase = Bookcase(name="Bookcase A")
|
||||||
|
shelf_1 = BookcaseShelf(row=0, column=0, bookcase=bookcase)
|
||||||
|
shelf_2 = BookcaseShelf(row=1, column=1, bookcase=bookcase)
|
||||||
|
sql_session.add_all([bookcase, shelf_1, shelf_2])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_bookcase_shelf_positions(sql_session, bookcase, column=1)
|
||||||
|
|
||||||
|
assert result == [(1, 1)]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_bookcase_shelf_positions_filters_by_row(sql_session: Session) -> None:
|
||||||
|
bookcase = Bookcase(name="Bookcase A")
|
||||||
|
shelf_1 = BookcaseShelf(row=0, column=0, bookcase=bookcase)
|
||||||
|
shelf_2 = BookcaseShelf(row=1, column=1, bookcase=bookcase)
|
||||||
|
sql_session.add_all([bookcase, shelf_1, shelf_2])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_bookcase_shelf_positions(sql_session, bookcase, row=1)
|
||||||
|
|
||||||
|
assert result == [(1, 1)]
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import (
|
||||||
|
Bookcase,
|
||||||
|
BookcaseItem,
|
||||||
|
BookcaseItemBorrowing,
|
||||||
|
BookcaseShelf,
|
||||||
|
MediaType,
|
||||||
|
)
|
||||||
|
from worblehat.queries.borrowing import (
|
||||||
|
has_active_borrowing,
|
||||||
|
list_active_borrowings,
|
||||||
|
list_active_borrowings_for_item,
|
||||||
|
list_borrowings_for_isbn,
|
||||||
|
list_overdue_borrowings,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_bookcase_item(
|
||||||
|
sql_session: Session,
|
||||||
|
name: str = "Some Book",
|
||||||
|
isbn: str = "1234567890",
|
||||||
|
) -> BookcaseItem:
|
||||||
|
"""Creates a BookcaseItem along with the MediaType/Bookcase/BookcaseShelf it needs."""
|
||||||
|
media_type = MediaType(name=f"Media type for {name}")
|
||||||
|
bookcase = Bookcase(name=f"Bookcase for {name}")
|
||||||
|
shelf = BookcaseShelf(row=0, column=0, bookcase=bookcase)
|
||||||
|
sql_session.add_all([media_type, bookcase, shelf])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
item = BookcaseItem(name, isbn)
|
||||||
|
item.media_type = media_type
|
||||||
|
item.shelf = shelf
|
||||||
|
sql_session.add(item)
|
||||||
|
sql_session.flush()
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_active_borrowings_excludes_delivered(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
active = BookcaseItemBorrowing("alice", item)
|
||||||
|
delivered = BookcaseItemBorrowing("bob", item)
|
||||||
|
delivered.delivered = datetime.now()
|
||||||
|
sql_session.add_all([active, delivered])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_active_borrowings(sql_session)
|
||||||
|
|
||||||
|
assert result == [active]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_active_borrowings_orders_by_end_time(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
later = BookcaseItemBorrowing("alice", item)
|
||||||
|
later.end_time = datetime.now() + timedelta(days=10)
|
||||||
|
sooner = BookcaseItemBorrowing("bob", item)
|
||||||
|
sooner.end_time = datetime.now() + timedelta(days=1)
|
||||||
|
sql_session.add_all([later, sooner])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_active_borrowings(sql_session)
|
||||||
|
|
||||||
|
assert result == [sooner, later]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_active_borrowings_for_item_scopes_to_item(sql_session: Session) -> None:
|
||||||
|
item_a = _make_bookcase_item(sql_session, name="Book A", isbn="1111111111")
|
||||||
|
item_b = _make_bookcase_item(sql_session, name="Book B", isbn="2222222222")
|
||||||
|
borrowing_a = BookcaseItemBorrowing("alice", item_a)
|
||||||
|
borrowing_b = BookcaseItemBorrowing("bob", item_b)
|
||||||
|
sql_session.add_all([borrowing_a, borrowing_b])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_active_borrowings_for_item(sql_session, item_a)
|
||||||
|
|
||||||
|
assert result == [borrowing_a]
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_active_borrowing_true_when_undelivered_borrowing_exists(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
sql_session.add(BookcaseItemBorrowing("alice", item))
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
assert has_active_borrowing(sql_session, "alice", item) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_active_borrowing_false_when_delivered(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
borrowing = BookcaseItemBorrowing("alice", item)
|
||||||
|
borrowing.delivered = datetime.now()
|
||||||
|
sql_session.add(borrowing)
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
assert has_active_borrowing(sql_session, "alice", item) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_active_borrowing_false_for_other_user(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
sql_session.add(BookcaseItemBorrowing("alice", item))
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
assert has_active_borrowing(sql_session, "bob", item) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_borrowings_for_isbn_scopes_to_isbn(sql_session: Session) -> None:
|
||||||
|
item_a = _make_bookcase_item(sql_session, name="Book A", isbn="1111111111")
|
||||||
|
item_b = _make_bookcase_item(sql_session, name="Book B", isbn="2222222222")
|
||||||
|
borrowing_a = BookcaseItemBorrowing("alice", item_a)
|
||||||
|
borrowing_b = BookcaseItemBorrowing("bob", item_b)
|
||||||
|
sql_session.add_all([borrowing_a, borrowing_b])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_borrowings_for_isbn(sql_session, "1111111111")
|
||||||
|
|
||||||
|
assert result == [borrowing_a]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_borrowings_for_isbn_orders_by_username(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
borrowing_bob = BookcaseItemBorrowing("bob", item)
|
||||||
|
borrowing_alice = BookcaseItemBorrowing("alice", item)
|
||||||
|
sql_session.add_all([borrowing_bob, borrowing_alice])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_borrowings_for_isbn(sql_session, item.isbn)
|
||||||
|
|
||||||
|
assert result == [borrowing_alice, borrowing_bob]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_overdue_borrowings_only_returns_undelivered_past_deadline(
|
||||||
|
sql_session: Session,
|
||||||
|
) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
|
||||||
|
overdue = BookcaseItemBorrowing("alice", item)
|
||||||
|
overdue.end_time = datetime.now() - timedelta(days=1)
|
||||||
|
|
||||||
|
not_yet_due = BookcaseItemBorrowing("bob", item)
|
||||||
|
not_yet_due.end_time = datetime.now() + timedelta(days=1)
|
||||||
|
|
||||||
|
overdue_but_delivered = BookcaseItemBorrowing("carol", item)
|
||||||
|
overdue_but_delivered.end_time = datetime.now() - timedelta(days=1)
|
||||||
|
overdue_but_delivered.delivered = datetime.now()
|
||||||
|
|
||||||
|
sql_session.add_all([overdue, not_yet_due, overdue_but_delivered])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_overdue_borrowings(sql_session)
|
||||||
|
|
||||||
|
assert result == [overdue]
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import (
|
||||||
|
Bookcase,
|
||||||
|
BookcaseItem,
|
||||||
|
BookcaseItemBorrowingQueue,
|
||||||
|
BookcaseShelf,
|
||||||
|
MediaType,
|
||||||
|
)
|
||||||
|
from worblehat.queries.borrowing_queue import (
|
||||||
|
is_in_borrowing_queue,
|
||||||
|
list_all_queue_items,
|
||||||
|
list_pending_queue_items_for_item,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_bookcase_item(
|
||||||
|
sql_session: Session,
|
||||||
|
name: str = "Some Book",
|
||||||
|
isbn: str = "1234567890",
|
||||||
|
) -> BookcaseItem:
|
||||||
|
"""Creates a BookcaseItem along with the MediaType/Bookcase/BookcaseShelf it needs."""
|
||||||
|
media_type = MediaType(name=f"Media type for {name}")
|
||||||
|
bookcase = Bookcase(name=f"Bookcase for {name}")
|
||||||
|
shelf = BookcaseShelf(row=0, column=0, bookcase=bookcase)
|
||||||
|
sql_session.add_all([media_type, bookcase, shelf])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
item = BookcaseItem(name, isbn)
|
||||||
|
item.media_type = media_type
|
||||||
|
item.shelf = shelf
|
||||||
|
sql_session.add(item)
|
||||||
|
sql_session.flush()
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_all_queue_items_orders_by_entered_queue_time(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
later = BookcaseItemBorrowingQueue("alice", item)
|
||||||
|
later.entered_queue_time = datetime.now() + timedelta(hours=1)
|
||||||
|
sooner = BookcaseItemBorrowingQueue("bob", item)
|
||||||
|
sooner.entered_queue_time = datetime.now()
|
||||||
|
sql_session.add_all([later, sooner])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_all_queue_items(sql_session)
|
||||||
|
|
||||||
|
assert result == [sooner, later]
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_in_borrowing_queue_true_when_present(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
sql_session.add(BookcaseItemBorrowingQueue("alice", item))
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
assert is_in_borrowing_queue(sql_session, "alice", item) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_in_borrowing_queue_false_for_other_user(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
sql_session.add(BookcaseItemBorrowingQueue("alice", item))
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
assert is_in_borrowing_queue(sql_session, "bob", item) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_in_borrowing_queue_false_when_empty(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
sql_session.add(item)
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
assert is_in_borrowing_queue(sql_session, "alice", item) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_pending_queue_items_for_item_excludes_available_items(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
pending = BookcaseItemBorrowingQueue("alice", item)
|
||||||
|
became_available = BookcaseItemBorrowingQueue("bob", item)
|
||||||
|
became_available.item_became_available_time = datetime.now()
|
||||||
|
sql_session.add_all([pending, became_available])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_pending_queue_items_for_item(sql_session, item)
|
||||||
|
|
||||||
|
assert result == [pending]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_pending_queue_items_for_item_scopes_to_item(sql_session: Session) -> None:
|
||||||
|
item_a = _make_bookcase_item(sql_session, name="Book A", isbn="1111111111")
|
||||||
|
item_b = _make_bookcase_item(sql_session, name="Book B", isbn="2222222222")
|
||||||
|
queue_item_a = BookcaseItemBorrowingQueue("alice", item_a)
|
||||||
|
queue_item_b = BookcaseItemBorrowingQueue("bob", item_b)
|
||||||
|
sql_session.add_all([queue_item_a, queue_item_b])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_pending_queue_items_for_item(sql_session, item_a)
|
||||||
|
|
||||||
|
assert result == [queue_item_a]
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import (
|
||||||
|
Bookcase,
|
||||||
|
BookcaseItem,
|
||||||
|
BookcaseItemBorrowing,
|
||||||
|
BookcaseItemBorrowingQueue,
|
||||||
|
BookcaseShelf,
|
||||||
|
DeadlineDaemonLastRunDatetime,
|
||||||
|
MediaType,
|
||||||
|
)
|
||||||
|
from worblehat.queries.deadline_daemon import (
|
||||||
|
find_last_run,
|
||||||
|
find_next_queue_position,
|
||||||
|
list_close_deadline_borrowings,
|
||||||
|
list_expiring_queue_positions,
|
||||||
|
list_newly_available_queue_items,
|
||||||
|
list_overdue_queue_positions,
|
||||||
|
list_undelivered_overdue_borrowings,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_bookcase_item(
|
||||||
|
sql_session: Session,
|
||||||
|
name: str = "Some Book",
|
||||||
|
isbn: str = "1234567890",
|
||||||
|
) -> BookcaseItem:
|
||||||
|
"""Creates a BookcaseItem along with the MediaType/Bookcase/BookcaseShelf it needs."""
|
||||||
|
media_type = MediaType(name=f"Media type for {name}")
|
||||||
|
bookcase = Bookcase(name=f"Bookcase for {name}")
|
||||||
|
shelf = BookcaseShelf(row=0, column=0, bookcase=bookcase)
|
||||||
|
sql_session.add_all([media_type, bookcase, shelf])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
item = BookcaseItem(name, isbn)
|
||||||
|
item.media_type = media_type
|
||||||
|
item.shelf = shelf
|
||||||
|
sql_session.add(item)
|
||||||
|
sql_session.flush()
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_last_run_returns_none_when_db_is_empty(sql_session: Session) -> None:
|
||||||
|
assert find_last_run(sql_session) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_last_run_returns_the_single_row(sql_session: Session) -> None:
|
||||||
|
last_run = DeadlineDaemonLastRunDatetime(time=datetime.now())
|
||||||
|
sql_session.add(last_run)
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
assert find_last_run(sql_session) is last_run
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_close_deadline_borrowings_matches_borrowings_ending_in_n_days(
|
||||||
|
sql_session: Session,
|
||||||
|
) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
close = BookcaseItemBorrowing("alice", item)
|
||||||
|
close.end_time = now + timedelta(days=2)
|
||||||
|
|
||||||
|
far = BookcaseItemBorrowing("bob", item)
|
||||||
|
far.end_time = now + timedelta(days=20)
|
||||||
|
|
||||||
|
sql_session.add_all([close, far])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_close_deadline_borrowings(
|
||||||
|
sql_session,
|
||||||
|
day=2,
|
||||||
|
last_run_datetime=now - timedelta(minutes=1),
|
||||||
|
current_run_datetime=now + timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == [close]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_close_deadline_borrowings_excludes_delivered(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
delivered = BookcaseItemBorrowing("alice", item)
|
||||||
|
delivered.end_time = now + timedelta(days=2)
|
||||||
|
delivered.delivered = now
|
||||||
|
|
||||||
|
sql_session.add(delivered)
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_close_deadline_borrowings(
|
||||||
|
sql_session,
|
||||||
|
day=2,
|
||||||
|
last_run_datetime=now - timedelta(minutes=1),
|
||||||
|
current_run_datetime=now + timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_undelivered_overdue_borrowings_only_returns_undelivered_past_deadline(
|
||||||
|
sql_session: Session,
|
||||||
|
) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
overdue = BookcaseItemBorrowing("alice", item)
|
||||||
|
overdue.end_time = now - timedelta(days=1)
|
||||||
|
|
||||||
|
not_yet_due = BookcaseItemBorrowing("bob", item)
|
||||||
|
not_yet_due.end_time = now + timedelta(days=1)
|
||||||
|
|
||||||
|
overdue_but_delivered = BookcaseItemBorrowing("carol", item)
|
||||||
|
overdue_but_delivered.end_time = now - timedelta(days=1)
|
||||||
|
overdue_but_delivered.delivered = now
|
||||||
|
|
||||||
|
sql_session.add_all([overdue, not_yet_due, overdue_but_delivered])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_undelivered_overdue_borrowings(sql_session, now)
|
||||||
|
|
||||||
|
assert result == [overdue]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_newly_available_queue_items_requires_delivery_in_window(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
borrowing = BookcaseItemBorrowing("alice", item)
|
||||||
|
borrowing.delivered = now
|
||||||
|
queue_item = BookcaseItemBorrowingQueue("bob", item)
|
||||||
|
|
||||||
|
sql_session.add_all([borrowing, queue_item])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_newly_available_queue_items(
|
||||||
|
sql_session,
|
||||||
|
last_run_datetime=now - timedelta(minutes=1),
|
||||||
|
current_run_datetime=now + timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == [queue_item]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_newly_available_queue_items_excludes_delivery_outside_window(
|
||||||
|
sql_session: Session,
|
||||||
|
) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
borrowing = BookcaseItemBorrowing("alice", item)
|
||||||
|
borrowing.delivered = now - timedelta(days=10)
|
||||||
|
queue_item = BookcaseItemBorrowingQueue("bob", item)
|
||||||
|
|
||||||
|
sql_session.add_all([borrowing, queue_item])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_newly_available_queue_items(
|
||||||
|
sql_session,
|
||||||
|
last_run_datetime=now - timedelta(minutes=1),
|
||||||
|
current_run_datetime=now + timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_newly_available_queue_items_excludes_undelivered_borrowings(
|
||||||
|
sql_session: Session,
|
||||||
|
) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
borrowing = BookcaseItemBorrowing("alice", item)
|
||||||
|
queue_item = BookcaseItemBorrowingQueue("bob", item)
|
||||||
|
|
||||||
|
sql_session.add_all([borrowing, queue_item])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_newly_available_queue_items(
|
||||||
|
sql_session,
|
||||||
|
last_run_datetime=now - timedelta(minutes=1),
|
||||||
|
current_run_datetime=now + timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_newly_available_queue_items_excludes_expired_queue_entries(
|
||||||
|
sql_session: Session,
|
||||||
|
) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
borrowing = BookcaseItemBorrowing("alice", item)
|
||||||
|
borrowing.delivered = now
|
||||||
|
queue_item = BookcaseItemBorrowingQueue("bob", item)
|
||||||
|
queue_item.expired = True
|
||||||
|
|
||||||
|
sql_session.add_all([borrowing, queue_item])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_newly_available_queue_items(
|
||||||
|
sql_session,
|
||||||
|
last_run_datetime=now - timedelta(minutes=1),
|
||||||
|
current_run_datetime=now + timedelta(minutes=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_expiring_queue_positions_matches_positions_in_window(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
sql_session.add(BookcaseItemBorrowing("someone", item))
|
||||||
|
|
||||||
|
in_window = BookcaseItemBorrowingQueue("alice", item)
|
||||||
|
in_window.item_became_available_time = now
|
||||||
|
|
||||||
|
out_of_window = BookcaseItemBorrowingQueue("bob", item)
|
||||||
|
out_of_window.item_became_available_time = now - timedelta(days=100)
|
||||||
|
|
||||||
|
sql_session.add_all([in_window, out_of_window])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_expiring_queue_positions(
|
||||||
|
sql_session,
|
||||||
|
last_run_datetime=now - timedelta(days=1),
|
||||||
|
current_run_datetime=now + timedelta(days=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == [in_window]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_expiring_queue_positions_ignores_items_without_a_borrowing(
|
||||||
|
sql_session: Session,
|
||||||
|
) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
queue_item = BookcaseItemBorrowingQueue("alice", item)
|
||||||
|
queue_item.item_became_available_time = now
|
||||||
|
sql_session.add(queue_item)
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_expiring_queue_positions(
|
||||||
|
sql_session,
|
||||||
|
last_run_datetime=now - timedelta(days=1),
|
||||||
|
current_run_datetime=now + timedelta(days=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_overdue_queue_positions_matches_positions_older_than_expiry(
|
||||||
|
sql_session: Session,
|
||||||
|
) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
overdue = BookcaseItemBorrowingQueue("alice", item)
|
||||||
|
overdue.item_became_available_time = now - timedelta(days=5)
|
||||||
|
|
||||||
|
not_overdue = BookcaseItemBorrowingQueue("bob", item)
|
||||||
|
not_overdue.item_became_available_time = now
|
||||||
|
|
||||||
|
sql_session.add_all([overdue, not_overdue])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_overdue_queue_positions(
|
||||||
|
sql_session,
|
||||||
|
queue_position_expiry_days=1,
|
||||||
|
current_run_datetime=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == [overdue]
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_overdue_queue_positions_excludes_already_expired(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
already_expired = BookcaseItemBorrowingQueue("alice", item)
|
||||||
|
already_expired.item_became_available_time = now - timedelta(days=5)
|
||||||
|
already_expired.expired = True
|
||||||
|
|
||||||
|
sql_session.add(already_expired)
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = list_overdue_queue_positions(
|
||||||
|
sql_session,
|
||||||
|
queue_position_expiry_days=1,
|
||||||
|
current_run_datetime=now,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_next_queue_position_returns_earliest_pending_entry(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
later = BookcaseItemBorrowingQueue("alice", item)
|
||||||
|
later.entered_queue_time = now + timedelta(hours=1)
|
||||||
|
sooner = BookcaseItemBorrowingQueue("bob", item)
|
||||||
|
sooner.entered_queue_time = now
|
||||||
|
|
||||||
|
sql_session.add_all([later, sooner])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = find_next_queue_position(sql_session, item.uid)
|
||||||
|
|
||||||
|
assert result is sooner
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_next_queue_position_skips_entries_that_already_became_available(
|
||||||
|
sql_session: Session,
|
||||||
|
) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
already_available = BookcaseItemBorrowingQueue("alice", item)
|
||||||
|
already_available.entered_queue_time = now
|
||||||
|
already_available.item_became_available_time = now
|
||||||
|
|
||||||
|
pending = BookcaseItemBorrowingQueue("bob", item)
|
||||||
|
pending.entered_queue_time = now + timedelta(hours=1)
|
||||||
|
|
||||||
|
sql_session.add_all([already_available, pending])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = find_next_queue_position(sql_session, item.uid)
|
||||||
|
|
||||||
|
assert result is pending
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_next_queue_position_returns_none_when_empty(sql_session: Session) -> None:
|
||||||
|
item = _make_bookcase_item(sql_session)
|
||||||
|
|
||||||
|
result = find_next_queue_position(sql_session, item.uid)
|
||||||
|
|
||||||
|
assert result is None
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import pytest
|
||||||
|
from sqlalchemy.exc import NoResultFound
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from worblehat.models import MediaType
|
||||||
|
from worblehat.queries.media_type import find_media_type_by_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_media_type_by_name_returns_match_case_insensitively(sql_session: Session) -> None:
|
||||||
|
book = MediaType(name="Book", description="A physical book")
|
||||||
|
comic = MediaType(name="Comic", description="A comic book")
|
||||||
|
sql_session.add_all([book, comic])
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
result = find_media_type_by_name(sql_session, "book")
|
||||||
|
|
||||||
|
assert result is book
|
||||||
|
|
||||||
|
|
||||||
|
def test_find_media_type_by_name_raises_when_missing(sql_session: Session) -> None:
|
||||||
|
sql_session.add(MediaType(name="Comic"))
|
||||||
|
sql_session.flush()
|
||||||
|
|
||||||
|
with pytest.raises(NoResultFound):
|
||||||
|
find_media_type_by_name(sql_session, "book")
|
||||||
@@ -4,29 +4,29 @@ requires-python = ">=3.12"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alembic"
|
name = "alembic"
|
||||||
version = "1.18.4"
|
version = "1.18.5"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "mako" },
|
{ name = "mako" },
|
||||||
{ name = "sqlalchemy" },
|
{ name = "sqlalchemy" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/1a/cc/ac0bed8e562e7407fe55c3ba85a4dce86e6dbd8730887bd1e406a6c5c18a/alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e", size = 2060480, upload-time = "2026-06-25T15:20:54.888Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" },
|
{ url = "https://files.pythonhosted.org/packages/96/78/5fe6dc3a3a5b2f5a2a4faef8bfe336d5fa049a38884ab3172e0098160c01/alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc", size = 264664, upload-time = "2026-06-25T15:20:56.673Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "beautifulsoup4"
|
name = "beautifulsoup4"
|
||||||
version = "4.14.3"
|
version = "4.15.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "soupsieve" },
|
{ name = "soupsieve" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
|
{ url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -40,80 +40,84 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2026.1.4"
|
version = "2026.7.22"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
|
{ url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "charset-normalizer"
|
name = "charset-normalizer"
|
||||||
version = "3.4.4"
|
version = "3.4.9"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/bd/2a/23f34ec9d04624958e137efdc394888716353190e75f25dd22c7a2c7a8aa/charset_normalizer-3.4.9.tar.gz", hash = "sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b", size = 152439, upload-time = "2026-07-07T14:34:58.454Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/4a/ecbd131485c07fcdfad54e28946d513e3da22ef3b4bd854dcafae54ec739/charset_normalizer-3.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0", size = 319300, upload-time = "2026-07-07T14:33:15.666Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/96/5d9364e3342d69f3a045e1777bc47c85c383e6e9466d561b33fdb419d1f9/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9", size = 215802, upload-time = "2026-07-07T14:33:17.031Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
|
{ url = "https://files.pythonhosted.org/packages/4b/4c/5361f9aa7f2cb58d94f2ab831b3d493f69efb1d239654b4744e3c09527cb/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44", size = 237171, upload-time = "2026-07-07T14:33:18.576Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
|
{ url = "https://files.pythonhosted.org/packages/50/78/ce342ca4ff30b2eb49fe6d9578df85974f90c67d294113e94efdd9664cbd/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9", size = 233075, upload-time = "2026-07-07T14:33:20.084Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
|
{ url = "https://files.pythonhosted.org/packages/01/c4/4fa4c8b3097a11f3c5f09a35b72ed6855fb1d332469504962ab7bafcc702/charset_normalizer-3.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd", size = 224256, upload-time = "2026-07-07T14:33:21.747Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
|
{ url = "https://files.pythonhosted.org/packages/87/3a/ad914516df7e358a81aae018caa5e0470ba827fa6d763b1d2e87d920a5f6/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84", size = 208784, upload-time = "2026-07-07T14:33:23.313Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
|
{ url = "https://files.pythonhosted.org/packages/d7/74/3c12f9755717dfe5c5c87da63f35d765fa0c00382ec26bf23f7fae34f2ba/charset_normalizer-3.4.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b", size = 219928, upload-time = "2026-07-07T14:33:24.814Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/9a/895095b83e7907abd6d3d99aad3a38ad0d9686cc186cb0c94c24320fe63e/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde", size = 218489, upload-time = "2026-07-07T14:33:26.42Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
|
{ url = "https://files.pythonhosted.org/packages/a1/34/ef5c05f412f42520d7709b7d3784d19640839eb7366ded1755511585429f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39", size = 210267, upload-time = "2026-07-07T14:33:27.952Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
|
{ url = "https://files.pythonhosted.org/packages/83/dc/9b29fa4412b318bf3bfea985c35d67eb55e04b59a7c3f2237168b0e0be6f/charset_normalizer-3.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62", size = 226030, upload-time = "2026-07-07T14:33:29.397Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
|
{ url = "https://files.pythonhosted.org/packages/0e/42/6dbc00b8cd16011691203e33570fa42ed5746599a2e878112d16eab403a3/charset_normalizer-3.4.9-cp312-cp312-win32.whl", hash = "sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642", size = 151185, upload-time = "2026-07-07T14:33:30.781Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
|
{ url = "https://files.pythonhosted.org/packages/80/cc/f920afd1a23c58ccd53c1d36085a71893a4737ff5e66e0371efab6809850/charset_normalizer-3.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0", size = 162557, upload-time = "2026-07-07T14:33:32.176Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
|
{ url = "https://files.pythonhosted.org/packages/f0/e6/0386d43a261ff4e4b30c5857af7df877254b46bec7b9d1b74b6bf969a90b/charset_normalizer-3.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2", size = 152665, upload-time = "2026-07-07T14:33:33.711Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
|
{ url = "https://files.pythonhosted.org/packages/b2/06/97ec2aeae780b31d742b6352218b43841a6871e2564578ca522dce4a45c3/charset_normalizer-3.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614", size = 317688, upload-time = "2026-07-07T14:33:35.408Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
|
{ url = "https://files.pythonhosted.org/packages/d0/39/8ff066c672434225f8d25f8b739f992af250944392173dcc88362681c9bf/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698", size = 214982, upload-time = "2026-07-07T14:33:36.996Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/8f/3a47a3667c83c2df9483d91644c6c107de3bf8874aa1793da9d3012eb986/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b", size = 236460, upload-time = "2026-07-07T14:33:38.536Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/60/b22cdbee7e4013dab8b0d7647fc6181120fbbbc8f7025c226d15bd5a47fc/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9", size = 232003, upload-time = "2026-07-07T14:33:40.059Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
|
{ url = "https://files.pythonhosted.org/packages/ea/f8/72eb13dcabe7257035cea8aefd922caad2f110d252bf9f67c4c2ca763aee/charset_normalizer-3.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33", size = 223149, upload-time = "2026-07-07T14:33:41.631Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/3e/faee8f9de92b14ee1198e9163252bb15efee7301b31256a3b6d9ebfdd0dd/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63", size = 207901, upload-time = "2026-07-07T14:33:43.209Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
|
{ url = "https://files.pythonhosted.org/packages/3a/25/45f30093ae27dd7b92a793b61882a38685f993700113ca36e0c9c14965e1/charset_normalizer-3.4.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0", size = 219176, upload-time = "2026-07-07T14:33:44.725Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
|
{ url = "https://files.pythonhosted.org/packages/48/18/c8f397329c35e32f6a837e488986f4ae03bd2abebc453b48714991630c2f/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe", size = 217356, upload-time = "2026-07-07T14:33:46.192Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
|
{ url = "https://files.pythonhosted.org/packages/86/7e/5ce0bba863470fd1902d5e5843968951bddf38abe4742fc97116ef4598b3/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35", size = 209614, upload-time = "2026-07-07T14:33:47.705Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
|
{ url = "https://files.pythonhosted.org/packages/6c/ef/2473d3c4d869155be4af1191111d59c4d5c4e0173026f7e85b176e23bf65/charset_normalizer-3.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8", size = 224991, upload-time = "2026-07-07T14:33:49.238Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
|
{ url = "https://files.pythonhosted.org/packages/d0/a3/53ddae3db108a088156aa8ddfafd411ebbc1340f48c5573f697b27f69a39/charset_normalizer-3.4.9-cp313-cp313-win32.whl", hash = "sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9", size = 150622, upload-time = "2026-07-07T14:33:50.711Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
|
{ url = "https://files.pythonhosted.org/packages/e8/ef/6953a77c7cf2c2ff9998e6f575ab3e380119f100223381565a4f94c1f836/charset_normalizer-3.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115", size = 161947, upload-time = "2026-07-07T14:33:52.197Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
|
{ url = "https://files.pythonhosted.org/packages/6e/fb/d560d1d1555debbfe7849d9cac6145c1b537709d79576bf22557ed803b82/charset_normalizer-3.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012", size = 152594, upload-time = "2026-07-07T14:33:53.486Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
|
{ url = "https://files.pythonhosted.org/packages/7e/8d/496817fa0944239ecae662dd57ea765cfeaec6a735f9f025d4b7b72e7143/charset_normalizer-3.4.9-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380", size = 317253, upload-time = "2026-07-07T14:33:54.994Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
|
{ url = "https://files.pythonhosted.org/packages/2b/f9/ef4a69ea338ad3c0deceea0f5f7d2380ae8b52132b06d652cb0d2cd86706/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9", size = 215898, upload-time = "2026-07-07T14:33:56.334Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
|
{ url = "https://files.pythonhosted.org/packages/8c/e7/5ddfd76fc061eb52de219658a4aa431cbacadf0a0219c8854f00da50d289/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4", size = 236718, upload-time = "2026-07-07T14:33:57.9Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
|
{ url = "https://files.pythonhosted.org/packages/49/ba/768fa3f36048d81c477a0ce61f813bc1454d80917ccfe550abd9f44f5e24/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a", size = 232519, upload-time = "2026-07-07T14:33:59.811Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
|
{ url = "https://files.pythonhosted.org/packages/f4/c4/b3e049d2aa3766180c78507110543d9d50894cc97f57de543f1be521dcdc/charset_normalizer-3.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046", size = 223143, upload-time = "2026-07-07T14:34:01.517Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
|
{ url = "https://files.pythonhosted.org/packages/19/79/55c32d06d76ae4feafe053f061f3e3ab70bcf19f4007797ce8c3efda7830/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81", size = 206742, upload-time = "2026-07-07T14:34:03.04Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
|
{ url = "https://files.pythonhosted.org/packages/10/e0/47c079dd82d217c807479cd59ffd30af56307ea31c108b75758970459ad3/charset_normalizer-3.4.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917", size = 219191, upload-time = "2026-07-07T14:34:04.657Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
|
{ url = "https://files.pythonhosted.org/packages/42/ab/b9bc2e77d6b44a7e46ef62ec5cac1c9a6ba7b9135a5d560f002696ec9995/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41", size = 218328, upload-time = "2026-07-07T14:34:06.115Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/78/c9c71d599f5aa2d42bcdd35cbbd46d7f535351a57e40ff7d8e5a7e219401/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1", size = 207406, upload-time = "2026-07-07T14:34:07.554Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
|
{ url = "https://files.pythonhosted.org/packages/f6/39/c914445c321a845097ce4f6ac7de9a18228a77b766272125a1ce00d851eb/charset_normalizer-3.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf", size = 225157, upload-time = "2026-07-07T14:34:09.061Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/f2/c0d4b8508565a36bc5c624e88ed297f5b0b1095011034d7f5b83a69908b5/charset_normalizer-3.4.9-cp314-cp314-win32.whl", hash = "sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48", size = 151095, upload-time = "2026-07-07T14:34:10.901Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
|
{ url = "https://files.pythonhosted.org/packages/49/fd/a1d26144398c67486422a72bf5812cda22cb4ccfcd95a290fb41ceb4b8e2/charset_normalizer-3.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b", size = 162796, upload-time = "2026-07-07T14:34:12.47Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/95/d75e82f8ce9fd323ebf059c16c9aadefb22a1ecde13b7840b35835e4886c/charset_normalizer-3.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519", size = 153334, upload-time = "2026-07-07T14:34:14.044Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
|
{ url = "https://files.pythonhosted.org/packages/00/5e/17398df3a139985ba9d11ed072531986f408c8fca952835ef1ab1820c02b/charset_normalizer-3.4.9-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198", size = 338848, upload-time = "2026-07-07T14:34:15.688Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
|
{ url = "https://files.pythonhosted.org/packages/cd/91/7253a32e86b7e1d1239b1b36ba6dd0f021a21107ab33054b53119cc083b9/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32", size = 223022, upload-time = "2026-07-07T14:34:17.248Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/32/2e64bd2be10e89c61e57ebe6a93fd98ae88eb7ebe414b5121f22c96c69eb/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632", size = 241590, upload-time = "2026-07-07T14:34:18.813Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
|
{ url = "https://files.pythonhosted.org/packages/3d/ef/d96ec496cfea0c21db43b0ad03891308b02388d054cc902cf0e5a1ad6a88/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf", size = 239584, upload-time = "2026-07-07T14:34:20.52Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
|
{ url = "https://files.pythonhosted.org/packages/d4/ce/9af95f7876194bd7a14e3dfe4a4de2e0bff02666a3910d72beafd06cc297/charset_normalizer-3.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990", size = 230224, upload-time = "2026-07-07T14:34:22.189Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
|
{ url = "https://files.pythonhosted.org/packages/52/94/af74dde74a3996bd959c350709bfe50e297823d70a8c1cbd54b838880863/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d", size = 212667, upload-time = "2026-07-07T14:34:23.857Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
|
{ url = "https://files.pythonhosted.org/packages/ee/f0/f1c4fe746c395922961b5916ed1d7d6e7d4c84851d19ed43cc89980ec953/charset_normalizer-3.4.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e", size = 227179, upload-time = "2026-07-07T14:34:25.586Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
|
{ url = "https://files.pythonhosted.org/packages/e4/56/6c745619ac397e8871e2bcd3cea1eec86b877488f33888b3aef5c3ed506e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c", size = 225372, upload-time = "2026-07-07T14:34:27.212Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
|
{ url = "https://files.pythonhosted.org/packages/78/ad/98aae8630ac71f16711968e38a5acfecce41b778bf2f0312851020f565a8/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2", size = 215222, upload-time = "2026-07-07T14:34:28.774Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
|
{ url = "https://files.pythonhosted.org/packages/f7/40/9593d54209765207a7f11073c06494c1721e4ca4a0a426c597679bf7f91e/charset_normalizer-3.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534", size = 231958, upload-time = "2026-07-07T14:34:30.345Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/27/693ee5e8a18191eb38647360c51cd505013e2bd3b366aa43fd5344c21e3c/charset_normalizer-3.4.9-cp314-cp314t-win32.whl", hash = "sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226", size = 155580, upload-time = "2026-07-07T14:34:31.884Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/3f/bd97d3d9c613013d07cb7733d299385b41df37f0471310f5a73dc359f0b8/charset_normalizer-3.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177", size = 167620, upload-time = "2026-07-07T14:34:33.438Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/c6/eee9dca4439b1061f76373f06ea855678cc4a64c1c3c90b50e479edbb8eb/charset_normalizer-3.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501", size = 158037, upload-time = "2026-07-07T14:34:35.018Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/2b/f97f1c193fb855c345d678f5077d6926034db0722df74c8f057020e05a25/charset_normalizer-3.4.9-py3-none-any.whl", hash = "sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5", size = 64538, upload-time = "2026-07-07T14:34:56.993Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.3.1"
|
version = "8.4.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
|
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -125,9 +129,91 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coverage"
|
||||||
|
version = "7.15.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/76/d0/55fe630f4cf94e3fcba868240fad8c8cdd1f764e2a932f8926347e6ec4cd/coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d", size = 927741, upload-time = "2026-07-15T18:56:19.558Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/50/eb5bf42e531611a9f8d272556b1ed4de503f84a91413584094487cf69f8f/coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9", size = 221587, upload-time = "2026-07-15T18:54:18.439Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/d1/da99af464c335d4e023a6efcd7ec30f63b88a43c93745154ab74ffb31cea/coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73", size = 221943, upload-time = "2026-07-15T18:54:20.062Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/8a/13c42723d61ca447eafa18732e8141dd6a63f2732e1c7e1502c182dd88d7/coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d", size = 253450, upload-time = "2026-07-15T18:54:21.765Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/29/99021303f98fbdcb63504b4d07bea4cc025b9b2dd907c4f07c85d50a0dab/coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b", size = 256187, upload-time = "2026-07-15T18:54:23.4Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/a8/fd503715ed6ca9c5d742923aa5209257340b367a867b2ced0c7d4ba8a0b9/coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296", size = 257301, upload-time = "2026-07-15T18:54:25.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/40/3f4b8fb409810036ebc2857d36adc0498c6e957b5df0290c5036b2e143f1/coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6", size = 259562, upload-time = "2026-07-15T18:54:27.204Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/8a/9bdffbef47db77cce3d6b02a28f7e919b19f0106c4b080c2c2246040f885/coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098", size = 253841, upload-time = "2026-07-15T18:54:29.134Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/1e/9031efde019d31a06646261fce6dfc5c3c74e951e27a71e5c9a424563178/coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a", size = 255221, upload-time = "2026-07-15T18:54:31.142Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/db/787acde872389fc84a9ef9d8cd1ccc658e391ab4cb5b28092a714426a394/coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b", size = 253366, upload-time = "2026-07-15T18:54:32.886Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/9b/6f57bc4b93c842eef1695f8cdaf2318e35e7ba54f5ba80d84be213ab7858/coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2", size = 257434, upload-time = "2026-07-15T18:54:34.7Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/26/b3186a21b2acc83e451118978905c81c7072c3333707804db09a78c096a2/coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440", size = 252935, upload-time = "2026-07-15T18:54:36.548Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/c2/c9f3376b2e717ea69ed7a6e9a5fcab968fb0b290db6cf4bd9a1fc7541b75/coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e", size = 254807, upload-time = "2026-07-15T18:54:38.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/e1/dfc15401f4a8aaeb486e1ba3e9e3c40522a6e38bd0ecf0b3f29cb8082957/coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd", size = 223641, upload-time = "2026-07-15T18:54:40.103Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/40/81b6d809d320cd366ec5bdf8176575e897dcb8efe7fb4b489ef9e93e4d13/coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40", size = 224172, upload-time = "2026-07-15T18:54:41.882Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/28/9f14ec438149f7de557f45518f09b4a7917b795cc37083aa7db482693f8c/coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3", size = 223556, upload-time = "2026-07-15T18:54:43.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/d5/f8c838e6b7282976f7c918884b792df7a0c42c5bba5d99c60ad2d221d56d/coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8", size = 221606, upload-time = "2026-07-15T18:54:45.448Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/37/97c926376364f66298cc44893b89cdf17b8bc406376497c4061ae4b8a8ff/coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1", size = 221982, upload-time = "2026-07-15T18:54:47.341Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/30/a36050a6e83c2135ee0776f452ca3948224befc6d7f26acecc082d0c106a/coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578", size = 252972, upload-time = "2026-07-15T18:54:49.2Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/d3/06b5f1daf95f0f15ab05bd75f26ba5f3c8b33d0bb72f3aaa3cf41d1bad3a/coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1", size = 255569, upload-time = "2026-07-15T18:54:51.098Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/1c/9afb3f8de2b8d36960391c48559a2e3ff96594b58099f115921549ea8d0d/coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6", size = 256806, upload-time = "2026-07-15T18:54:53.145Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/d8/b989f96061a5e32d82fddd1b1b9ff48a7c8f8ae7606f0e80fd9de54b1e33/coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7", size = 258936, upload-time = "2026-07-15T18:54:55.015Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/fa/f99771f5110457c7b511c1935ca49ddf288218eaa84322e028b9334146ae/coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d", size = 253178, upload-time = "2026-07-15T18:54:57.527Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/96/c098a6044d119c751ceede7be91035fa8310170ec24a6523aff72f0a5793/coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026", size = 254934, upload-time = "2026-07-15T18:54:59.41Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/a2/1457b3a7a50c8d77500103b97a046db863e2f59a1cf6d2f814595f349885/coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa", size = 252898, upload-time = "2026-07-15T18:55:01.338Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/0e/76958874c471ecfcdde0d2b2747bb2c61bdbf34a40636f4ce9db9923e643/coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d", size = 257056, upload-time = "2026-07-15T18:55:03.243Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/7c/3d7c4e3bf58baa40327dc7edc2272b17cf02299366d52763db1b0ca1556a/coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b", size = 252718, upload-time = "2026-07-15T18:55:05.029Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/b8/1cecffed9ce14fb25be9ba42d37b6bb61485c9a3ddd43cd3dde36b6087d8/coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188", size = 254490, upload-time = "2026-07-15T18:55:06.889Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/2c/42984561bc7f4c045dca67516a0c50ee5ef8d84352dbeb5559dc86c4823e/coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050", size = 223647, upload-time = "2026-07-15T18:55:08.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/9f/39c7c9245efc583beddf89a87683574e663ed93637f3afb6cd7b88405676/coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c", size = 224190, upload-time = "2026-07-15T18:55:10.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/de/3a2883cf8a213659280ef4b403059e17a9acaeb7fc7fd4105e1226ff2e6d/coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b", size = 223583, upload-time = "2026-07-15T18:55:12.678Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/5f/aed265fd7a3551a394f36dfe41868aee709b7f95db4052205b4ad1563ac3/coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a", size = 221650, upload-time = "2026-07-15T18:55:14.527Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/2c/222ba12a545189017120f8eddfc1a0bd4616b47d5d4a8d99421edb2fe4c6/coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2", size = 221988, upload-time = "2026-07-15T18:55:16.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/38/304b5877ab46e6c290b4292cfcf3fe28245f0e5597cad7f6acc91fc7e0a4/coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138", size = 253029, upload-time = "2026-07-15T18:55:18.856Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/58/821b533b8db9e44cf1d8a97bd525149ced40dde1d0093da02cb78e715244/coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f", size = 255536, upload-time = "2026-07-15T18:55:21.027Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/f2/7aa06604c389d32ea7f0a6a988359a7eafc3cd3f8e7bc2e88cd2fdf0b877/coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984", size = 256881, upload-time = "2026-07-15T18:55:23.125Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a2/4f/1ef342339c7916d0096bc5888cc0f653882cc7bc8f897d5cb89143287c9b/coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7", size = 259196, upload-time = "2026-07-15T18:55:25.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/f4/7ed055d7a9c5ec13b161773a115a5ccc6b0081d568c31fad830806306cc7/coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3", size = 253036, upload-time = "2026-07-15T18:55:27.018Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/79/ea82cca18c242a3a38b6c017da39726aa62dcb64aa635abf79b92009975c/coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee", size = 254887, upload-time = "2026-07-15T18:55:29.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/ba/a136db3c0d9562b00e10b72540dbf3a33cd3bc5b95060c9308e247494623/coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1", size = 252852, upload-time = "2026-07-15T18:55:31.184Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/17/ea334246b16b7d059953fad6fdefa11e33c68efbd3fe37b1098120a1fac2/coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a", size = 257128, upload-time = "2026-07-15T18:55:33.163Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/c3/074fb66d46d607855f710876b117cbda562c5ab08363528e78820449f937/coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145", size = 252668, upload-time = "2026-07-15T18:55:35.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/c1/f620850ada9b36435921c9a3a8057013422b1d964eb4bf37fe138724d192/coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446", size = 254325, upload-time = "2026-07-15T18:55:37.125Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/31/a729ca3689404493af82ef8e6ff70bd88bdda8da89aeef6ca9b387aeb2b4/coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243", size = 223844, upload-time = "2026-07-15T18:55:39.078Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/83/5d809dc808fb1698c671f3e372259bb9158e64b7ea526fc6ab7de64de9fe/coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc", size = 224331, upload-time = "2026-07-15T18:55:41.346Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/4e/35e488548e952795829e129995c4174df33bf432b591d1aa42c8d9e4e7ad/coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635", size = 223760, upload-time = "2026-07-15T18:55:43.518Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/49/dd2c86cd6374038f6e415fb5bfb86db5218553209c081384a020369dee79/coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be", size = 222384, upload-time = "2026-07-15T18:55:45.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/74/173ff17a1c0808e5a438f549f6f145d5ac7528f2791310b63523e3200ac7/coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072", size = 222647, upload-time = "2026-07-15T18:55:47.544Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/f8/b8cba872162356fb44ac79c10309d987206a4461e32072fc29228dad7331/coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328", size = 264013, upload-time = "2026-07-15T18:55:49.768Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/67/a807a7586d0b8cae485308ddd55756f0806c92f8e0b411bacbf23c48edf3/coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a", size = 266135, upload-time = "2026-07-15T18:55:51.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/67/cd78771dc985f7e4ebdcc82b1a96d9a932af9e806f01f2f91a89f4c72e80/coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0", size = 268555, upload-time = "2026-07-15T18:55:54.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/3e/10134cf81275188c58568f324fc74aedff32c63ca4d5bbc513a91944a6f0/coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5", size = 269674, upload-time = "2026-07-15T18:55:56.066Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/4a/771b77de446cba985dc414bbc5844bd21604da05dbc044286df8318a48a7/coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743", size = 263101, upload-time = "2026-07-15T18:55:58.107Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/b5/70a7011da15f4071943361183aefa27847f3e3aec4fd335f1cb3d3a622b1/coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c", size = 266007, upload-time = "2026-07-15T18:56:00.468Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/0d/f9547e804ce7ad49646ffeffac26699510efbe6c0f751b66fdc960c4e825/coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071", size = 263611, upload-time = "2026-07-15T18:56:02.615Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/59/f576a396659c0efd351f5c1544f67c3560e89c7761cabf7f65e412beeda5/coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a", size = 267344, upload-time = "2026-07-15T18:56:04.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/5d/c2e4fce3579c0cb635024293f1a32bbe26df101b3e3a69f22243d1352b6c/coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d", size = 262456, upload-time = "2026-07-15T18:56:06.641Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/dd/956287d69436b66094bc4b57ac2da71e43bfd2a5524e958900b9f582fcf8/coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c", size = 264771, upload-time = "2026-07-15T18:56:08.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/5a/6f979530c2734c575de77cf58f5f28d51f7123a94b5030fd9156fe5f363c/coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688", size = 224151, upload-time = "2026-07-15T18:56:10.856Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/7e/27f6b2a74d484742f4017553e710b01e396b23d809df3e95ca0bb9a2824b/coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199", size = 224981, upload-time = "2026-07-15T18:56:12.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/48/284863423aa474240f6842bd00d680da22f4e6ea2e466618ef7c9c9e69a9/coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658", size = 224294, upload-time = "2026-07-15T18:56:15.156Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/82/32e3bd191d498e64f6f911ad55d14006a0861e54869d2d32452326399e65/coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c", size = 213375, upload-time = "2026-07-15T18:56:17.305Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coverage-badge"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "coverage" },
|
||||||
|
{ name = "setuptools" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/be/8f/e92b0a010c76b0da82709838b3f3ae9aec638d0c44dbfb1186a5751f5d2e/coverage_badge-1.1.2.tar.gz", hash = "sha256:fe7ed58a3b72dad85a553b64a99e963dea3847dcd0b8ddd2b38a00333618642c", size = 6335, upload-time = "2024-08-02T23:34:08.58Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/3d/5642a1a06191b2e1e0f87a2e824e6d3eb7c32c589a68ed4d1dcbd3324d63/coverage_badge-1.1.2-py2.py3-none-any.whl", hash = "sha256:d8413ce51c91043a1692b943616b450868cbeeb0ea6a0c54a32f8318c9c96ff7", size = 6493, upload-time = "2024-08-02T23:34:07.063Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flask"
|
name = "flask"
|
||||||
version = "3.1.2"
|
version = "3.1.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "blinker" },
|
{ name = "blinker" },
|
||||||
@@ -137,14 +223,14 @@ dependencies = [
|
|||||||
{ name = "markupsafe" },
|
{ name = "markupsafe" },
|
||||||
{ name = "werkzeug" },
|
{ name = "werkzeug" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/26/00/35d85dcce6c57fdc871f3867d465d780f302a175ea360f62533f12b27e2b/flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb", size = 759004, upload-time = "2026-02-19T05:00:57.678Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/9c/34f6962f9b9e9c71f6e5ed806e0d0ff03c9d1b0b2340088a0cf4bce09b18/flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c", size = 103424, upload-time = "2026-02-19T05:00:56.027Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flask-admin"
|
name = "flask-admin"
|
||||||
version = "2.0.2"
|
version = "2.2.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "flask" },
|
{ name = "flask" },
|
||||||
@@ -153,9 +239,9 @@ dependencies = [
|
|||||||
{ name = "werkzeug" },
|
{ name = "werkzeug" },
|
||||||
{ name = "wtforms" },
|
{ name = "wtforms" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/b7/78a1534b4fe1a40bccf79e8e274e1ace6ee1678c804e4c178b1c96e6c8d6/flask_admin-2.0.2.tar.gz", hash = "sha256:1d06aec7efee957972b43f6b08a0bd08d5f4cf9a337d4ece2f17c98abc2a214e", size = 5528977, upload-time = "2025-11-11T21:59:14.782Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/61/08/63770603ea3184782d6996455dcf9520893db58e9bd9f3e346b52d91fcea/flask_admin-2.2.0.tar.gz", hash = "sha256:4a5b844789c10076da89320563600a7addf781e73ab315ae800521b3ec018509", size = 5145370, upload-time = "2026-05-07T09:18:01.969Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/25/a379b8dc388630f1d173544c8b3ebbdb2ad5b60992e531b724d7f8a7ea03/flask_admin-2.0.2-py3-none-any.whl", hash = "sha256:4b3c44068de0fe4630dfcd190cc11231cbbdd7bac315c74c55d1764087b8b273", size = 6459099, upload-time = "2025-11-11T21:59:12.82Z" },
|
{ url = "https://files.pythonhosted.org/packages/81/80/e68f7b341303acd34eea4ec15428355b30300aecd90cc4f91cb9722a280b/flask_admin-2.2.0-py3-none-any.whl", hash = "sha256:a8cb0f484eefb60b1ed345105ebb7199664ff910c52f02081b7881c390f643d3", size = 6056010, upload-time = "2026-05-07T09:17:59.331Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -173,50 +259,75 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "3.3.1"
|
version = "3.5.4"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/a3/74/b13368064b09053253555d3f2839cc2684d22d5aed0d2ccffbf7a6736558/greenlet-3.5.4.tar.gz", hash = "sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20", size = 206538, upload-time = "2026-07-22T12:47:14.468Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" },
|
{ url = "https://files.pythonhosted.org/packages/f3/04/81bd731d6d1e3a469d9a4c36f5eb069bcf0cbb2d5d342c9fec22245b91fc/greenlet-3.5.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4", size = 295909, upload-time = "2026-07-22T11:38:09.261Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" },
|
{ url = "https://files.pythonhosted.org/packages/cc/dd/f5f22903a6ae70f5ea328ed0beaec92ad903f0e3b7d2845133b354abc4b8/greenlet-3.5.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17", size = 612011, upload-time = "2026-07-22T12:26:40.69Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" },
|
{ url = "https://files.pythonhosted.org/packages/8e/10/92a4a88d12b915d74ea5b6d288e4afefda4771647caa34442c156f7a454f/greenlet-3.5.4-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a", size = 624299, upload-time = "2026-07-22T12:29:02.089Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" },
|
{ url = "https://files.pythonhosted.org/packages/50/6d/0b14bb9db2989f32cd9fe7f76afedea01ee8bee3f87c07e69f24adfe7e63/greenlet-3.5.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f", size = 621541, upload-time = "2026-07-22T11:51:09.464Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" },
|
{ url = "https://files.pythonhosted.org/packages/48/3d/25e9a2d9eb6b2e8b7ca4e80a3a26cb887cce6c8e0a87c921164f11bc5574/greenlet-3.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d", size = 1581444, upload-time = "2026-07-22T12:25:03.818Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" },
|
{ url = "https://files.pythonhosted.org/packages/b9/96/4c9bf2e2c408dcc0556edce69efa9f802e82223573c53240136a086821f1/greenlet-3.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9", size = 1645842, upload-time = "2026-07-22T11:51:12.295Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" },
|
{ url = "https://files.pythonhosted.org/packages/b5/41/303ecb26a3a56122c0f4d4073ee078881847bd6b6f463ae0ec57ec20223b/greenlet-3.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3", size = 247169, upload-time = "2026-07-22T11:38:19.893Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" },
|
{ url = "https://files.pythonhosted.org/packages/a4/e3/ef56864b4c35fcb3eb3b41b869f6cc46f4cd3f5e2c68e74acde8ac433951/greenlet-3.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:77d6ce04fed0d9aeed42e0f37923cc43eba9b027bdd9c34546bb4ccd143d0fe0", size = 245565, upload-time = "2026-07-22T11:38:27.061Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" },
|
{ url = "https://files.pythonhosted.org/packages/c0/9a/e51225dcd58713f16ccbdcc501a8da21098ea14515b7870f1f94459e5ff5/greenlet-3.5.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02", size = 294831, upload-time = "2026-07-22T11:38:53.389Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" },
|
{ url = "https://files.pythonhosted.org/packages/9f/ea/de50a50fadf979713ab18b46f22ad5ff5f2dcfc637a3ebdecf669801e1a5/greenlet-3.5.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356", size = 614619, upload-time = "2026-07-22T12:26:42.282Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/c7/2aae27fea41205b8650294c301f042a2a4bb6155eea48c995b890a92f2c1/greenlet-3.5.4-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef", size = 627021, upload-time = "2026-07-22T12:29:03.445Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/56/79fd826f9ccaae0b84e1b4ef68dabba5e105bb044ffcd448a0b782fcba9a/greenlet-3.5.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0", size = 624002, upload-time = "2026-07-22T11:51:11.391Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" },
|
{ url = "https://files.pythonhosted.org/packages/0a/1a/27319f97e731298513dcba1a2e91b63e9d8811d9de22130f960b129b1bf1/greenlet-3.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd", size = 1581533, upload-time = "2026-07-22T12:25:05.322Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" },
|
{ url = "https://files.pythonhosted.org/packages/b1/6d/24240bf562e9786dd2799ee0a4a4dadb4ded22510f41b20245099159ac8c/greenlet-3.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f", size = 1645781, upload-time = "2026-07-22T11:51:14.805Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" },
|
{ url = "https://files.pythonhosted.org/packages/c1/5a/442ab1a9ef7ca6bf7210e5397a95972206a91a31033a03c8900866a10039/greenlet-3.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c", size = 247133, upload-time = "2026-07-22T11:39:20.661Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/e6/9160210222386b1a378ff94db846b9508ca24a121cf684991561fdb69280/greenlet-3.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f", size = 245500, upload-time = "2026-07-22T11:40:22.185Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/a5/a7/6ab1d4f9cd548d15ab90da29947f2076100130bb179b0bde59f795a459e3/greenlet-3.5.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22", size = 295410, upload-time = "2026-07-22T11:40:35.747Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" },
|
{ url = "https://files.pythonhosted.org/packages/cd/7a/422f63b4715cbc0b24385305407adf38b48f6bb68b3e6b04090e994d0f5a/greenlet-3.5.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf", size = 661286, upload-time = "2026-07-22T12:26:43.8Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" },
|
{ url = "https://files.pythonhosted.org/packages/d0/31/5a1cac663bf5582190c5a714ef81364f03cde232227f39748f8ae4c11da5/greenlet-3.5.4-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9", size = 673517, upload-time = "2026-07-22T12:29:04.815Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" },
|
{ url = "https://files.pythonhosted.org/packages/15/4a/2a82a1e3f8aaca020853ac8d12211280ca2b231aa08ea39f636f1060c319/greenlet-3.5.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8", size = 670917, upload-time = "2026-07-22T11:51:13.589Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" },
|
{ url = "https://files.pythonhosted.org/packages/68/b0/e379a152b17bfdfa95795af4049e37c0fd1b4d81f020d426db104ed07c77/greenlet-3.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3", size = 1628478, upload-time = "2026-07-22T12:25:06.678Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" },
|
{ url = "https://files.pythonhosted.org/packages/5e/43/bffdfa64f7317f954c5c1230b5dd5922676ce198689a68c1ac1ed4b1b1a5/greenlet-3.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec", size = 1692021, upload-time = "2026-07-22T11:51:17.008Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/cb/c21a3fd5d2c9c8b622e7bede6d6d00e00551a5ee474ea6d831b5f567a8b4/greenlet-3.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:96aff77af063b607f2489473484e39a0bbae730f2ea90c9e5606c9b73c44174a", size = 228125, upload-time = "2026-01-23T15:32:45.265Z" },
|
{ url = "https://files.pythonhosted.org/packages/d0/11/f799f9637e2c6e9b0b716015e339040598b058cf7654dfc0d67468b177ed/greenlet-3.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c", size = 248031, upload-time = "2026-07-22T11:40:11.007Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/8e/8a2db6d11491837af1de64b8aff23707c6e85241be13c60ed399a72e2ef8/greenlet-3.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:b066e8b50e28b503f604fa538adc764a638b38cf8e81e025011d26e8a627fa79", size = 227519, upload-time = "2026-01-23T15:31:47.284Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/75/625bcdd74d5e6b2dca1ecba3c3ac77bcf8a026c21a649a46cef23e421f97/greenlet-3.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:f260930bbbbcf9caee661211235a5111c86dfe5832fdf6ae4570da1e0995320f", size = 246892, upload-time = "2026-07-22T11:40:27.357Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/69/35c62ed49c320cb4d98e14698ccca5467d3bfe683984172be9cb564d9ce3/greenlet-3.5.4-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3", size = 305571, upload-time = "2026-07-22T11:40:31.659Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" },
|
{ url = "https://files.pythonhosted.org/packages/5b/6c/64d60216b3640dcb0b62d913dd9e0d80030c09115bb2e4ba70c95d10ca45/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867", size = 672568, upload-time = "2026-07-22T12:26:45.298Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" },
|
{ url = "https://files.pythonhosted.org/packages/5c/de/ba3ab0a96292e53039530333b0d2ae18d9e508f3a325cd7bf15f8172944c/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c", size = 680076, upload-time = "2026-07-22T12:29:06.125Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" },
|
{ url = "https://files.pythonhosted.org/packages/a5/be/aeada79083c6f1c15f45d77a332f9c441af263ee298e3eb17522cd337d22/greenlet-3.5.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66", size = 676733, upload-time = "2026-07-22T11:51:16.027Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/10/2392fc3a98948652ef5fd1e7275c04f861dd13f74b78a2b4309f4ee4d090/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7", size = 1637327, upload-time = "2026-07-22T12:25:07.879Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" },
|
{ url = "https://files.pythonhosted.org/packages/55/c6/e7237a3dfa1f205ed0d9ea1e46d70bd2811b32d516266399fb59d28ab90a/greenlet-3.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e", size = 1697493, upload-time = "2026-07-22T11:51:19.214Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" },
|
{ url = "https://files.pythonhosted.org/packages/55/e3/4ba8154ba2a3d43729e499f72471b4b5c993f3826d3e24da81d5f06d6572/greenlet-3.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132", size = 251637, upload-time = "2026-07-22T11:40:37.44Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/03/e3f96dfc100261a29545ddc8270cafe58f9195b6651466910e820910de77/greenlet-3.5.4-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809", size = 296076, upload-time = "2026-07-22T11:39:38.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/3d/da52d208e5c977bce8667e784729e584e38b5785f4c1ec0f4c836e9a1c42/greenlet-3.5.4-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927", size = 666870, upload-time = "2026-07-22T12:26:46.691Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/8a/7e6dee25cb8a8cf9b362c8e597cc269593378bd916f16c736c059a52e85a/greenlet-3.5.4-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5", size = 677678, upload-time = "2026-07-22T12:29:07.508Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/21/5a38699fa45de749e3857d93b8f07e4c20489e77c2d35d915a2e1c456606/greenlet-3.5.4-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0", size = 676067, upload-time = "2026-07-22T11:51:18.163Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/ba/863116ab8ff1ca7a729e327800268939d182db47aa433db70e216e7d9194/greenlet-3.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88", size = 1633489, upload-time = "2026-07-22T12:25:09.605Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/06/7466ced82818d6132462d7f26b3f83c66ea15d2b193a6c0088d558ed7d95/greenlet-3.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c", size = 1696584, upload-time = "2026-07-22T11:51:21.304Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/4d/55b638489260065de9ffce606c8b5d04507bef705de4b212a0c3d6a1a0df/greenlet-3.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da", size = 248297, upload-time = "2026-07-22T11:42:04.055Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/08/9dd4ae635da93d41dc268bc34bd62a9d711ed8b8825c5d22ac910c7d6e6d/greenlet-3.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:f908898d6fa484ce4b6f447ce70ea99b52c503fee419e53cf74d60a16bc9e667", size = 247423, upload-time = "2026-07-22T11:44:00.764Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/66/7c87ed9cdbf1d49c2c6cd1c7b9dd4d16c33b24235ca03972293a1876b30c/greenlet-3.5.4-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c", size = 306487, upload-time = "2026-07-22T11:41:25.118Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/05/0a4201e7c0054866eefc05da234f236dd4c950d0fbf9ca0517141f01b269/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9", size = 676479, upload-time = "2026-07-22T12:26:48.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/c0/4b6b8c5a3aec70f0649cd89662d120fdd6421e2bdc8e3b15c3ab5ec568d8/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25", size = 684321, upload-time = "2026-07-22T12:29:08.925Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/c9/b49c31c9a972eee91e260445770e922244a7efc542697f51a012ec046d0f/greenlet-3.5.4-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde", size = 681293, upload-time = "2026-07-22T11:51:20.43Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/6f/7f2d4653770500eee667866016d42d7a68e3d3462f80df6b8e3fcd48a0eb/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8", size = 1642474, upload-time = "2026-07-22T12:25:10.819Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/d8/8cba31036a4caae448087ba5d150660ab03b4a0f54d9150f6495a3be7262/greenlet-3.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2", size = 1701012, upload-time = "2026-07-22T11:51:23.17Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/cd/3f77a4cce3bae631b08eb52f53a82a976669600337e21dfdba811cb50267/greenlet-3.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2", size = 251977, upload-time = "2026-07-22T11:41:38.125Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/e8/65e8707d00fe2a49bf12f609a9b2b39ba6dd23c2810eacad877c4fc94bfe/greenlet-3.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994", size = 250538, upload-time = "2026-07-22T11:40:17.985Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.11"
|
version = "3.18"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
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/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
||||||
wheels = [
|
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/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -252,21 +363,21 @@ wheels = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "libdib"
|
name = "libdib"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { git = "https://git.pvv.ntnu.no/Projects/libdib.git#7218348163fd8d84df4a6f682c634793e67a3fed" }
|
source = { git = "https://git.pvv.ntnu.no/Projects/libdib.git#2406de41ce9d0a1404cbf4e55537e3f720f37f23" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "sqlalchemy" },
|
{ name = "sqlalchemy" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mako"
|
name = "mako"
|
||||||
version = "1.3.10"
|
version = "1.3.12"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "markupsafe" },
|
{ name = "markupsafe" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" },
|
{ url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -332,6 +443,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
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/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pastel"
|
name = "pastel"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -341,58 +461,132 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" },
|
{ url = "https://files.pythonhosted.org/packages/aa/18/a8444036c6dd65ba3624c63b734d3ba95ba63ace513078e1580590075d21/pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364", size = 5955, upload-time = "2020-09-16T19:21:11.409Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "poethepoet"
|
name = "poethepoet"
|
||||||
version = "0.41.0"
|
version = "0.48.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pastel" },
|
{ name = "pastel" },
|
||||||
{ name = "pyyaml" },
|
{ name = "pyyaml" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a8/b9/fa92286560f70eaa40d473ea48376d20c6c21f63627d33c6bb1c5e385175/poethepoet-0.41.0.tar.gz", hash = "sha256:dcaad621dc061f6a90b17d091bebb9ca043d67bfe9bd6aa4185aea3ebf7ff3e6", size = 87780, upload-time = "2026-02-08T20:45:36.061Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/5c/92/93a4af9511b8c7c647874521d9e6c904266be98067c2ee1eb2e74520d208/poethepoet-0.48.0.tar.gz", hash = "sha256:a06f49d244fadfc2e2e7faa78b54e64a9694727e4ce1d50e08f23cea3ded74f1", size = 148679, upload-time = "2026-07-05T21:48:30.106Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/5d/5e/0b83e0222ce5921b3f9081eeca8c6fb3e1cfd5ca0d06338adf93b28ce061/poethepoet-0.41.0-py3-none-any.whl", hash = "sha256:4bab9fd8271664c5d21407e8f12827daeb6aa484dc6cc7620f0c3b4e62b42ee4", size = 113590, upload-time = "2026-02-08T20:45:34.697Z" },
|
{ url = "https://files.pythonhosted.org/packages/59/8d/d7c9455b15f8d2d7ce57e7b71a8ef8d02d9992ae4283c9777120620c9022/poethepoet-0.48.0-py3-none-any.whl", hash = "sha256:98da6096d060f49b8d84034770265863fb7dc92a40233b7694b9d216ac68737d", size = 185808, upload-time = "2026-07-05T21:48:28.601Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psycopg2-binary"
|
name = "psycopg2-binary"
|
||||||
version = "2.9.11"
|
version = "2.9.12"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/6c/8767aaa597ba424643dc87348c6f1754dd9f48e80fdc1b9f7ca5c3a7c213/psycopg2-binary-2.9.11.tar.gz", hash = "sha256:b6aed9e096bf63f9e75edf2581aa9a7e7186d97ab5c177aa6c87797cd591236c", size = 379620, upload-time = "2025-10-10T11:14:48.041Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" },
|
{ url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/fa/cae40e06849b6c9a95eb5c04d419942f00d9eaac8d81626107461e268821/psycopg2_binary-2.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f090b7ddd13ca842ebfe301cd587a76a4cf0913b1e429eb92c1be5dbeb1a19bc", size = 3864509, upload-time = "2025-10-10T11:11:56.452Z" },
|
{ url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" },
|
{ url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" },
|
{ url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" },
|
{ url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" },
|
{ url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" },
|
{ url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" },
|
{ url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" },
|
{ url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" },
|
{ url = "https://files.pythonhosted.org/packages/91/bb/4608c96f970f6e0c56572e87027ef4404f709382a3503e9934526d7ba051/psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965", size = 3712419, upload-time = "2026-04-20T23:34:58.754Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/e1/c2b38d256d0dafd32713e9f31982a5b028f4a3651f446be70785f484f472/psycopg2_binary-2.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:366df99e710a2acd90efed3764bb1e28df6c675d33a7fb40df9b7281694432ee", size = 3864529, upload-time = "2025-10-10T11:12:36.791Z" },
|
{ url = "https://files.pythonhosted.org/packages/5e/af/48f76af9d50d61cf390f8cd657b503168b089e2e9298e48465d029fcc713/psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7", size = 3822990, upload-time = "2026-04-20T23:35:00.821Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" },
|
{ url = "https://files.pythonhosted.org/packages/7a/df/aba0f99397cd811d32e06fc0cc781f1f3ce98bc0e729cb423925085d781a/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777", size = 4578696, upload-time = "2026-04-20T23:35:03.409Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" },
|
{ url = "https://files.pythonhosted.org/packages/95/9c/eaa74021ac4e4d5c2f83d82fc6615a63f4fe6c94dc4e94c3990427053f67/psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5", size = 4274982, upload-time = "2026-04-20T23:35:05.583Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" },
|
{ url = "https://files.pythonhosted.org/packages/35/ed/c25deff98bd26187ba48b3b250a3ffc3037c46c5b89362534a15d200e0db/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9", size = 5894867, upload-time = "2026-04-20T23:35:07.902Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" },
|
{ url = "https://files.pythonhosted.org/packages/9a/81/8d0e21ca77373c6c9589e5c4528f6e8f0c08c62cafc76fb0bddb7a2cee22/psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019", size = 4110578, upload-time = "2026-04-20T23:35:10.149Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" },
|
{ url = "https://files.pythonhosted.org/packages/00/fc/f481e2435bd8f742d0123309174aae4165160ad3ef17c1b99c3622c241d2/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c", size = 3655816, upload-time = "2026-04-20T23:35:12.56Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" },
|
{ url = "https://files.pythonhosted.org/packages/53/79/b9f46466bdbe9f239c96cde8be33c1aace4842f06013b47b730dc9759187/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f", size = 3301307, upload-time = "2026-04-20T23:35:15.029Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/19/7dc003b32fe35024df89b658104f7c8538a8b2dcbde7a4e746ce929742e7/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be", size = 3048968, upload-time = "2026-04-20T23:35:16.757Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" },
|
{ url = "https://files.pythonhosted.org/packages/91/58/2dbd7db5c604d45f4950d988506aae672a14126ec22998ced5021cbb76bb/psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290", size = 3351369, upload-time = "2026-04-20T23:35:18.933Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" },
|
{ url = "https://files.pythonhosted.org/packages/42/ee/dee8dcaad07f735824de3d6563bc67119fa6c28257b17977a8d624f02fab/psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0", size = 2757347, upload-time = "2026-04-20T23:35:21.283Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" },
|
{ url = "https://files.pythonhosted.org/packages/13/1b/708c0dca874acfad6d65314271859899a79007686f3a1f74e82a2ed4b645/psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2", size = 3712428, upload-time = "2026-04-20T23:35:23.453Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7c/a9/9d55c614a891288f15ca4b5209b09f0f01e3124056924e17b81b9fa054cc/psycopg2_binary-2.9.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e0deeb03da539fa3577fcb0b3f2554a97f7e5477c246098dbb18091a4a01c16f", size = 3864755, upload-time = "2025-10-10T11:13:17.727Z" },
|
{ url = "https://files.pythonhosted.org/packages/d6/39/ddbea9d4b4de6aca9431b6ed253f530f8a02d3b8f9bcfd0dbfe2b3de6fe4/psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2", size = 3823184, upload-time = "2026-04-20T23:35:25.92Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" },
|
{ url = "https://files.pythonhosted.org/packages/bf/a0/bc2fef74b106fa345567122a0659e6d94512ed7dc0131ec44c9e5aba3725/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f", size = 4579157, upload-time = "2026-04-20T23:35:28.542Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" },
|
{ url = "https://files.pythonhosted.org/packages/57/d7/d4e3b2005d3de607ca4fbb0e8742e248056e52184a6b94ebda3c1c2c329b/psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354", size = 4274970, upload-time = "2026-04-20T23:35:30.418Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" },
|
{ url = "https://files.pythonhosted.org/packages/2e/42/c9853f8db3967fe08bcde11f53d53b85d351750cae726ce001cb68afa9c1/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033", size = 5895175, upload-time = "2026-04-20T23:35:33.584Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/fd/b82b5601a97630308bef079f545ffec481bbbc795c2ba5ec416a01d03f60/psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e", size = 4110658, upload-time = "2026-04-20T23:35:35.638Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" },
|
{ url = "https://files.pythonhosted.org/packages/62/8c/32ca69b0389ef25dd22937bf9e8fbe2ce27aea20b05ded48c4ce4cb42475/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5", size = 3656251, upload-time = "2026-04-20T23:35:37.854Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" },
|
{ url = "https://files.pythonhosted.org/packages/c4/29/96992a2b59e3b9d730fcf9612d0a387305025dc867a9fc490a9e496e074e/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5", size = 3301810, upload-time = "2026-04-20T23:35:39.927Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" },
|
{ url = "https://files.pythonhosted.org/packages/56/ad/44b06659949b243ae10112cd3b20a197f9bf3e81d5651379b9eb889bfaad/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d", size = 3048977, upload-time = "2026-04-20T23:35:41.806Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" },
|
{ url = "https://files.pythonhosted.org/packages/1d/f2/10a1bcebadb6aa55e280e1f58975c36a7b560ea525184c7aa4064c466633/psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd", size = 3351466, upload-time = "2026-04-20T23:35:43.993Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" },
|
{ url = "https://files.pythonhosted.org/packages/20/be/b732c8418ffa5bcfda002890f5dc4c869fc17db66ff11f53b17cfe44afc0/psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980", size = 2848762, upload-time = "2026-04-20T23:35:46.421Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.20.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-cov"
|
||||||
|
version = "7.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "coverage" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-html"
|
||||||
|
version = "4.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jinja2" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-metadata" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c4/08/2076aa09507e51c1119d16a84c6307354d16270558f1a44fc9a2c99fdf1d/pytest_html-4.2.0.tar.gz", hash = "sha256:b6a88cba507500d8709959201e2e757d3941e859fd17cfd4ed87b16fc0c67912", size = 108634, upload-time = "2026-01-19T11:25:26.471Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/47/07046e0acedc12fe2bae79cf6c73ad67f51ae9d67df64d06b0f3eac73d36/pytest_html-4.2.0-py3-none-any.whl", hash = "sha256:ff5caf3e17a974008e5816edda61168e6c3da442b078a44f8744865862a85636", size = 23801, upload-time = "2026-01-19T11:25:25.008Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-metadata"
|
||||||
|
version = "3.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952, upload-time = "2024-02-12T19:38:44.887Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428, upload-time = "2024-02-12T19:38:42.531Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -443,7 +637,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.5"
|
version = "2.34.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "certifi" },
|
{ name = "certifi" },
|
||||||
@@ -451,90 +645,107 @@ dependencies = [
|
|||||||
{ name = "idna" },
|
{ name = "idna" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "83.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/34/26/f5d29e25ffdb535afef2d35cdb55b325298f96debd670da4c325e08d70f4/setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef", size = 1154254, upload-time = "2026-07-04T15:31:22.699Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "soupsieve"
|
name = "soupsieve"
|
||||||
version = "2.8.3"
|
version = "2.9.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/d9/38/e12680bbe6b4f8f3d17adcaf38d26850aa756c85cf4a80e79fc12a018fe8/soupsieve-2.9.1.tar.gz", hash = "sha256:c33e6605bbc71dd628b00c632d58ae607c22bade247e52553928f83bbb75b4ba", size = 122261, upload-time = "2026-07-21T16:57:17.452Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
|
{ url = "https://files.pythonhosted.org/packages/0f/2c/437fe806897c2d6cfdc3ee43a18da8bf8e568530a4ae9bac781541ca9896/soupsieve-2.9.1-py3-none-any.whl", hash = "sha256:4f4477399246b7a0c720a88ca2454b11cd6bb9ae4c9d170140786e916776c14c", size = 37404, upload-time = "2026-07-21T16:57:16.421Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlalchemy"
|
name = "sqlalchemy"
|
||||||
version = "2.0.46"
|
version = "2.0.51"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
{ name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/02/f1/a7a892f18d4d224e6b26f706531eafccc41e37594d37d304786969ee13cb/sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9", size = 9912201, upload-time = "2026-06-15T15:41:20.012Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" },
|
{ url = "https://files.pythonhosted.org/packages/d5/70/e868bc5412acd101a8280f25c95f10eeae0771c4eb806b02491142810ee8/sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a", size = 2160291, upload-time = "2026-06-15T16:08:48.918Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" },
|
{ url = "https://files.pythonhosted.org/packages/e5/1c/71ee0f8a6b9d7316a1ccd30430b4c62b6c2e36adc96017a4e3a72dce49d6/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e", size = 3343835, upload-time = "2026-06-15T16:19:42.613Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" },
|
{ url = "https://files.pythonhosted.org/packages/2b/7c/7ab9f9aadc5944fdd06612484ed7918fe376ad871a5f50404dc1536e0194/sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9", size = 3358470, upload-time = "2026-06-15T16:26:38.011Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" },
|
{ url = "https://files.pythonhosted.org/packages/d0/7d/ff77169fee6186de145a7f2b87006c39638391130abbab2b1f63ac6ea583/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389", size = 3289874, upload-time = "2026-06-15T16:19:45.212Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" },
|
{ url = "https://files.pythonhosted.org/packages/6f/3b/6c505903710d781b55bc3141ee34a062bf9745a6b5bc7333305b9ed63b33/sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d", size = 3321692, upload-time = "2026-06-15T16:26:39.747Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" },
|
{ url = "https://files.pythonhosted.org/packages/3c/b7/c5ffe50aa2f4d947c9250e1519d939260329a07fe6272edfccd784b3d007/sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5", size = 2119674, upload-time = "2026-06-15T16:23:09.543Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" },
|
{ url = "https://files.pythonhosted.org/packages/25/dc/46a65916af68a06ef6b972c6050ba4c8f97070fe3fb33097d34229d9bef6/sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080", size = 2146670, upload-time = "2026-06-15T16:23:11.048Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" },
|
{ url = "https://files.pythonhosted.org/packages/54/fe/a210d52fd1a90ecfae8a78e9d8b27e18d733d60818a8bf250ff690b75120/sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07", size = 2157184, upload-time = "2026-06-15T16:08:50.374Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" },
|
{ url = "https://files.pythonhosted.org/packages/17/6b/2dce8369b199cb855110e056032f94a9f66dacc2237d3d39c115a86eac56/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195", size = 3284735, upload-time = "2026-06-15T16:19:46.934Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" },
|
{ url = "https://files.pythonhosted.org/packages/53/ff/dbc495b8a14da840faffb353857a72d4190113cac33727906fb997047f0f/sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f", size = 3302756, upload-time = "2026-06-15T16:26:41.336Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" },
|
{ url = "https://files.pythonhosted.org/packages/cf/d5/fde8f4dddcf518ee15ab35a7c6a28acc32c8ba548d1d2aa451f96e6dbb0b/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400", size = 3232055, upload-time = "2026-06-15T16:19:49.286Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" },
|
{ url = "https://files.pythonhosted.org/packages/67/d1/43d3a0ac955a58601c24fa23038b1c55ee3a1ec02c0f96ebb1eae2bcf614/sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d", size = 3269850, upload-time = "2026-06-15T16:26:43.017Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" },
|
{ url = "https://files.pythonhosted.org/packages/94/df/de669c7054cd47c4439ac34b1b2ee8b804a794791fbb10720e997a2c87c7/sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b", size = 2117721, upload-time = "2026-06-15T16:23:12.36Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" },
|
{ url = "https://files.pythonhosted.org/packages/d0/8a/403c51d064196bae20a0bc2476577f83a3f8dd299719a97417086b7f2ec5/sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5", size = 2143615, upload-time = "2026-06-15T16:23:13.906Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" },
|
{ url = "https://files.pythonhosted.org/packages/b1/49/a739be2e1d02a96a658eb71ab45d921c874249252358ad24a5bffdd02525/sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491", size = 2158999, upload-time = "2026-06-15T16:08:51.759Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" },
|
{ url = "https://files.pythonhosted.org/packages/23/6b/2e0e38cf75c8780eca78d9b2e78164f8bcfd70125e5caa588ff5cbb9c9f4/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d", size = 3282539, upload-time = "2026-06-15T16:19:51.065Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" },
|
{ url = "https://files.pythonhosted.org/packages/dd/a1/e77854cb5336fd37dc3c6ae3b71de242c98caac5725120be0b526b31cbd0/sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54", size = 3287545, upload-time = "2026-06-15T16:26:44.735Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" },
|
{ url = "https://files.pythonhosted.org/packages/f6/ab/9e17272fd4dac8df3b83c4fbe52b998a1c9d89a843c8c35ff29b74ff7364/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e", size = 3230929, upload-time = "2026-06-15T16:19:52.625Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" },
|
{ url = "https://files.pythonhosted.org/packages/02/3c/52f408ea701781caee975606beccc48845f2aee8711ac29843d612c0306c/sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d", size = 3252888, upload-time = "2026-06-15T16:26:46.454Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" },
|
{ url = "https://files.pythonhosted.org/packages/24/16/3efd2ee6bc4ca4693a30a1dd17a91b606cae15d517d2a4746611d9b73ce8/sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8", size = 2120551, upload-time = "2026-06-15T16:23:15.629Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/78/55b12e70f45bccc40d9e483925c065027b3b98ea4cbbdf6f8c2546feaf6c/sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499", size = 2146318, upload-time = "2026-06-15T16:23:17.108Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d9/a0/2053b39e4e63b5d7ceb3372cface0859a067c1ddbd575ea7e9985716f771/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e84b09a9b0f19accedcbeff5c2caf36e0dd537341a33aad8d680336152dc34e", size = 3221994, upload-time = "2026-01-21T18:46:54.622Z" },
|
{ url = "https://files.pythonhosted.org/packages/21/db/a9574ed40fed418924b1b1a3e54f47ee3963053b3d3d325a0d36b41f2c08/sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de", size = 2178920, upload-time = "2026-06-15T15:59:56.285Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/87/97713497d9502553c68f105a1cb62786ba1ee91dea3852ae4067ed956a50/sqlalchemy-2.0.46-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4f52f7291a92381e9b4de9050b0a65ce5d6a763333406861e33906b8aa4906bf", size = 3243990, upload-time = "2026-01-21T18:40:18.253Z" },
|
{ url = "https://files.pythonhosted.org/packages/bf/90/a1bb5c7cbba76b7bc1fbd586d0a5479a7bc9c27b4a8298f22ec9423b2bb3/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7", size = 3566534, upload-time = "2026-06-15T15:58:35.024Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/87/5d1b23548f420ff823c236f8bea36b1a997250fd2f892e44a3838ca424f4/sqlalchemy-2.0.46-cp314-cp314-win32.whl", hash = "sha256:70ed2830b169a9960193f4d4322d22be5c0925357d82cbf485b3369893350908", size = 2114215, upload-time = "2026-01-21T18:42:55.232Z" },
|
{ url = "https://files.pythonhosted.org/packages/15/4b/481f1fed30e0e9e8dd24aecbb49f29eb57fe7657ece5cf06ee9b84bb97d8/sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72", size = 3535844, upload-time = "2026-06-15T16:02:43.973Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/20/555f39cbcf0c10cf452988b6a93c2a12495035f68b3dbd1a408531049d31/sqlalchemy-2.0.46-cp314-cp314-win_amd64.whl", hash = "sha256:3c32e993bc57be6d177f7d5d31edb93f30726d798ad86ff9066d75d9bf2e0b6b", size = 2139867, upload-time = "2026-01-21T18:42:56.474Z" },
|
{ url = "https://files.pythonhosted.org/packages/02/71/0aa64aeda645510af0a43f7d9ee70932f0d1dc4263aed34c50ee891d9df3/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23", size = 3475355, upload-time = "2026-06-15T15:58:36.592Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/f0/f96c8057c982d9d8a7a68f45d69c674bc6f78cad401099692fe16521640a/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dafb537740eef640c4d6a7c254611dca2df87eaf6d14d6a5fca9d1f4c3fc0fa", size = 3561202, upload-time = "2026-01-21T18:33:10.337Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/db/6061db32316446135a3abae5f308d144ab988a34234726042da3e58b1c63/sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522", size = 3486591, upload-time = "2026-06-15T16:02:45.346Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/53/3b37dda0a5b137f21ef608d8dfc77b08477bab0fe2ac9d3e0a66eaeab6fc/sqlalchemy-2.0.46-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42a1643dc5427b69aca967dae540a90b0fbf57eaf248f13a90ea5930e0966863", size = 3526296, upload-time = "2026-01-21T18:45:12.657Z" },
|
{ url = "https://files.pythonhosted.org/packages/0d/c9/f14fdf71bb8957e0c7e39db69bbdf12b5c80f4ef775fdfa127bf4e0d6760/sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7", size = 2151313, upload-time = "2026-06-15T16:03:39.127Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/75/f28622ba6dde79cd545055ea7bd4062dc934e0621f7b3be2891f8563f8de/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff33c6e6ad006bbc0f34f5faf941cfc62c45841c64c0a058ac38c799f15b5ede", size = 3470008, upload-time = "2026-01-21T18:33:11.725Z" },
|
{ url = "https://files.pythonhosted.org/packages/6a/c6/673e618e6f4f297e126d9b56ea2f6478708f6c1af4e3223835c22e2c3697/sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2", size = 2186280, upload-time = "2026-06-15T16:03:40.569Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/42/4afecbbc38d5e99b18acef446453c76eec6fbd03db0a457a12a056836e22/sqlalchemy-2.0.46-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82ec52100ec1e6ec671563bbd02d7c7c8d0b9e71a0723c72f22ecf52d1755330", size = 3476137, upload-time = "2026-01-21T18:45:15.001Z" },
|
{ url = "https://files.pythonhosted.org/packages/e2/22/dbf013a12ec759e54a34a119e9e217435b3f71b2dd5c61a7ade0a25dae87/sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5", size = 1944334, upload-time = "2026-06-15T16:09:22.418Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fc/a1/9c4efa03300926601c19c18582531b45aededfb961ab3c3585f1e24f120b/sqlalchemy-2.0.46-py3-none-any.whl", hash = "sha256:f9c11766e7e7c0a2767dda5acb006a118640c9fc0a4104214b96269bfb78399e", size = 1937882, upload-time = "2026-01-21T18:22:10.456Z" },
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlparse"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.15.0"
|
version = "4.16.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.6.3"
|
version = "2.7.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "werkzeug"
|
name = "werkzeug"
|
||||||
version = "3.1.5"
|
version = "3.1.8"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "markupsafe" },
|
{ name = "markupsafe" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" },
|
{ url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -560,14 +771,21 @@ dev = [
|
|||||||
{ name = "poethepoet" },
|
{ name = "poethepoet" },
|
||||||
{ name = "werkzeug" },
|
{ name = "werkzeug" },
|
||||||
]
|
]
|
||||||
|
test = [
|
||||||
|
{ name = "coverage-badge" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-cov" },
|
||||||
|
{ name = "pytest-html" },
|
||||||
|
{ name = "sqlparse" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "alembic", specifier = ">=1.17" },
|
{ name = "alembic", specifier = ">=1.16" },
|
||||||
{ name = "beautifulsoup4", specifier = ">=4.14" },
|
{ name = "beautifulsoup4", specifier = ">=4.14" },
|
||||||
{ name = "click", specifier = ">=8.3" },
|
{ name = "click", specifier = ">=8.2" },
|
||||||
{ name = "flask", specifier = ">=3.0" },
|
{ name = "flask", specifier = ">=3.0" },
|
||||||
{ name = "flask-admin", specifier = ">=2.0" },
|
{ name = "flask-admin", specifier = ">=1.6" },
|
||||||
{ name = "flask-sqlalchemy", specifier = ">=3.1" },
|
{ name = "flask-sqlalchemy", specifier = ">=3.1" },
|
||||||
{ name = "isbnlib", specifier = ">=3.10" },
|
{ name = "isbnlib", specifier = ">=3.10" },
|
||||||
{ name = "libdib", git = "https://git.pvv.ntnu.no/Projects/libdib.git" },
|
{ name = "libdib", git = "https://git.pvv.ntnu.no/Projects/libdib.git" },
|
||||||
@@ -581,15 +799,22 @@ dev = [
|
|||||||
{ name = "poethepoet" },
|
{ name = "poethepoet" },
|
||||||
{ name = "werkzeug" },
|
{ name = "werkzeug" },
|
||||||
]
|
]
|
||||||
|
test = [
|
||||||
|
{ name = "coverage-badge", specifier = ">=1.1.2" },
|
||||||
|
{ name = "pytest", specifier = ">=9.0.3" },
|
||||||
|
{ name = "pytest-cov", specifier = ">=7.1.0" },
|
||||||
|
{ name = "pytest-html", specifier = ">=4.2.0" },
|
||||||
|
{ name = "sqlparse", specifier = ">=0.5.5" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wtforms"
|
name = "wtforms"
|
||||||
version = "3.2.1"
|
version = "3.2.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "markupsafe" },
|
{ name = "markupsafe" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/01/e4/633d080897e769ed5712dcfad626e55dbd6cf45db0ff4d9884315c6a82da/wtforms-3.2.1.tar.gz", hash = "sha256:df3e6b70f3192e92623128123ec8dca3067df9cfadd43d59681e210cfb8d4682", size = 137801, upload-time = "2024-10-21T11:34:00.108Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/e9/91/ed9b517da898e3fb747566aa3c12a734bd64ea7449a0d25ec74ce8f8b8eb/wtforms-3.2.2.tar.gz", hash = "sha256:7b00c73f8670f35d4edb0293dcd81b980528bee72fd662b182aaba27ae570b93", size = 139583, upload-time = "2026-05-03T05:53:44.147Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/c9/2088fb5645cd289c99ebe0d4cdcc723922a1d8e1beaefb0f6f76dff9b21c/wtforms-3.2.1-py3-none-any.whl", hash = "sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4", size = 152454, upload-time = "2024-10-21T11:33:58.44Z" },
|
{ url = "https://files.pythonhosted.org/packages/18/76/bb225c8300f3a0ba28e01df51419c6c9574a297c43d71b29048e03b65deb/wtforms-3.2.2-py3-none-any.whl", hash = "sha256:72b90d5d921bd3119252069cf0301e9c13915f9e52792652bc91c5dda4b79e56", size = 158656, upload-time = "2026-05-03T05:53:46.072Z" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user