Consolidated commit message to v22 signals

This commit is contained in:
tanshu committed 2026-08-25 14:45:09 +00:00
1 parent 6403d25d3e
commit df289b20b8
443 files changed
+16057 -17331

No files matched your search

+31
View File
@@ -0,0 +1,31 @@
{
"mcpServers": {
"postgres-db": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-postgres",
"${POSTGRES_URL}"
],
"env": {
"POSTGRES_URL": "${env:SQLALCHEMY_DATABASE_URI}"
}
},
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"."
]
},
"angular-cli": {
"command": "npx",
"args": [
"-y",
"@angular/cli",
"mcp"
]
}
}
}
+30
View File
@@ -0,0 +1,30 @@
# Project Architecture & Guidelines
## Monorepo Layout
- **Backend**: `./brewman` (Python, managed via `uv`)
- **Frontend**: `./overlord` (Angular, managed via `npm`/`ng`)
- **Environment**: Root `.env` file containing shared/database variables.
---
## Execution Rules & Commands
### Backend (`/brewman`)
- **Working Directory**: Always execute backend commands within `./brewman` OR pass `--directory brewman` to `uv`.
- **Package & Script Management**:
- Use `uv` exclusively. Never run bare `pip` or global `python`.
- Run scripts/tests: `uv run python <script>` or `uv run pytest` from inside `./brewman`.
- Manage dependencies: `uv add <package>` or `uv remove <package>`.
- **Environment**: The root `.env` file (`../.env` relative to `brewman`) contains the database connection string and secret keys.
### Frontend (`/overlord`)
- **Working Directory**: Always execute frontend commands within `./overlord`.
- **Commands**:
- Run dev server: `npm start` or `ng serve`
- Code generation: `ng g c components/<name>` (prefer standalone components)
- Testing: `ng test --watch=false`
- Build: `ng build`
### Database (PostgreSQL)
- Database settings are defined in the root `.env`.
- Inspect schema, run migrations, or check types strictly via database tools or `uv run` scripts inside `./brewman`.
+117
View File
@@ -0,0 +1,117 @@
---
trigger: glob
globs: ./bookie/**/**
---
# Persona
You are a dedicated Angular developer who thrives on leveraging the absolute latest features of the framework to build cutting-edge applications. You are currently immersed in Angular v20+, passionately adopting signals for reactive state management, embracing standalone components for streamlined architecture, and utilizing the new control flow for more intuitive template logic. Performance is paramount to you, who constantly seeks to optimize change detection and improve user experience through these modern Angular paradigms. When prompted, assume You are familiar with all the newest APIs and best practices, valuing clean, efficient, and maintainable code.
## Examples
These are modern examples of how to write an Angular 20 component with signals
```ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
@Component({
selector: '{{tag-name}}-root',
templateUrl: '{{tag-name}}.html',
})
export class {{ClassName}} {
protected readonly isServerRunning = signal(true);
toggleServerStatus() {
this.isServerRunning.update(isServerRunning => !isServerRunning);
}
}
```
```css
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
button {
margin-top: 10px;
}
}
```
```html
<section class="container">
@if (isServerRunning()) {
<span>Yes, the server is running</span>
} @else {
<span>No, the server is not running</span>
}
<button (click)="toggleServerStatus()">Toggle Server Status</button>
</section>
```
When you update a component, be sure to put the logic in the ts file, the styles in the css file and the html template in the html file.
## Resources
Here are the some links to the essentials for building Angular applications. Use these to get an understanding of how some of the core functionality works
https://angular.dev/essentials/components
https://angular.dev/essentials/signals
https://angular.dev/essentials/templates
https://angular.dev/essentials/dependency-injection
## Best practices & Style guide
Here are the best practices and the style guide information.
### Coding Style guide
Here is a link to the most recent Angular style guide https://angular.dev/style-guide
### TypeScript Best Practices
- Use strict type checking
- Prefer type inference when the type is obvious
- Avoid the `any` type; use `unknown` when type is uncertain
### Angular Best Practices
- Always use standalone components over `NgModules`
- Do NOT set `standalone: true` inside the `@Component`, `@Directive` and `@Pipe` decorators
- Use signals for state management
- Implement lazy loading for feature routes
- Use `NgOptimizedImage` for all static images.
- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead
### Components
- Keep components small and focused on a single responsibility
- Use `input()` signal instead of decorators, learn more here https://angular.dev/guide/components/inputs
- Use `output()` function instead of decorators, learn more here https://angular.dev/guide/components/outputs
- Use `computed()` for derived state learn more about signals here https://angular.dev/guide/signals.
- Prefer inline templates for small components
- Prefer Reactive forms instead of Template-driven ones
- Do NOT use `ngClass`, use `class` bindings instead, for context: https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings
- Do NOT use `ngStyle`, use `style` bindings instead, for context: https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings
### State Management
- Use signals for local component state
- Use `computed()` for derived state
- Keep state transformations pure and predictable
- Do NOT use `mutate` on signals, use `update` or `set` instead
### Templates
- Keep templates simple and avoid complex logic
- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`
- Use the async pipe to handle observables
- Use built in pipes and import pipes when being used in a template, learn more https://angular.dev/guide/templates/pipes#
### Services
- Design services around a single responsibility
- Use the `providedIn: 'root'` option for singleton services
- Use the `inject()` function instead of constructor injection
+17
View File
@@ -0,0 +1,17 @@
---
name: angular-signals
description: Writes modern Angular code using Standalone Components and Signals. Use whenever generating frontend UI or state logic.
---
# Modern Angular Standards
## Architecture
- NEVER generate or use `NgModule`.
- ALL components, directives, and pipes must be `standalone: true`.
## State Management (Signals)
- Prefer Angular Signals (`signal()`, `computed()`, `effect()`) for synchronous, local component state instead of `BehaviorSubject`.
- Use the new Signal-based inputs and outputs: `input()` and `output()` instead of `@Input()` and `@Output()`.
- Use standard RxJS strictly for asynchronous streams (e.g., HTTP requests, router events).
## Control Flow
- Use the new built-in control flow (`@if`, `@for`, `@switch`) instead of structural directives (`*ngIf`, `*ngFor`).
@@ -0,0 +1,16 @@
---
name: fastapi-pydantic-v2
description: Writes modern FastAPI endpoints using Pydantic v2 syntax and Annotated dependencies. Use whenever generating or refactoring API routes.
---
# FastAPI & Pydantic v2 Standards
## Pydantic v2 Rules
- NEVER use `.dict()` or `.json()`. ALWAYS use `.model_dump()` and `.model_dump_json()`.
- NEVER use `@validator` or `@root_validator`. ALWAYS use `@field_validator` and `@model_validator(mode="before|after")`.
- NEVER use `schema_extra`. Use `json_schema_extra` inside the `model_config` dict.
- Use `pydantic_settings.BaseSettings` for all environment variable configurations.
## FastAPI Rules
- ALWAYS use `typing.Annotated` for dependency injection (e.g., `db: Annotated[AsyncSession, Depends(get_db)]`).
- Do not use `def` for endpoints that perform I/O; always use `async def`.
- Raise `fastapi.HTTPException` for expected client errors instead of returning standard dictionaries.
@@ -0,0 +1,4 @@
{
"kind": "tool-skill",
"version": "0.0.19"
}
+37
View File
@@ -0,0 +1,37 @@
---
name: library-skills
description: Use Library Skills to discover, install, refresh, repair, check, and manage agent skills from installed packages.
---
# Library Skills
Use this skill when a project might benefit from agent skills bundled by its installed packages, or when existing Library Skills-managed symlinks are stale, broken, orphaned, or need to be checked.
Run commands from the project root.
Agents bundle their own skills by including an `.agents/skills` directory. More details in [Library Skills](https://library-skills.io).
## First-Time Setup
- Make sure project dependencies are installed first, for example with `uv sync` for Python projects or `npm install` / `bun install` for Node.js projects.
- Run `uvx library-skills` or `npx library-skills` to discover skills bundled by the installed packages and install selected skills interactively.
- Use `uvx library-skills --all` or `npx library-skills --all` only when all newly discovered skills should be installed without selecting individual skills.
- Use `uvx library-skills --tool-skill` or `npx library-skills --tool-skill` to copy this Library Skills tool skill into the project so future agents know how to discover, install, update, repair, and check skills.
## Commands
- Run `uvx library-skills` or `npx library-skills` to discover package-provided skills, install selected new skills, and reconcile existing managed symlinks.
- Run `uvx library-skills list` or `npx library-skills list` to inspect discovered and installed skills.
- Run `uvx library-skills list --json` or `npx library-skills list --json` for machine-readable installed status.
- Run `uvx library-skills scan --json` or `npx library-skills scan --json` for discovery-only automation.
- Run `uvx library-skills --check` or `npx library-skills --check` to validate managed skill symlink state without changing files.
- Run `uvx library-skills --yes` or `npx library-skills --yes` to repair stale managed symlinks and remove orphaned managed symlinks non-interactively.
- Add `--claude` when `.claude/skills` should also be managed.
- Add `--skill NAME` to install a specific discovered skill by name.
## Safety
- Prefer rerunning `library-skills` over editing managed symlinks manually.
- If installed skill symlinks are broken, dependencies may not be installed yet. Try the project's normal install command first, such as `uv sync`, `npm install`, or `bun install`, then rerun `library-skills`.
- Do not delete or overwrite hand-authored skill directories.
- Library Skills only removes managed symlinks. It should not remove copied or hand-authored skill directories.
+19
View File
@@ -0,0 +1,19 @@
---
name: sqlalchemy-async
description: Writes SQLAlchemy 2.0 async database queries using Psycopg3. Use whenever interacting with the PostgreSQL database.
---
# SQLAlchemy 2.0 Async Standards
## Model Definitions
- ALWAYS use SQLAlchemy 2.0 Declarative Base with `Mapped` and `mapped_column`.
- Example: `id: Mapped[int] = mapped_column(primary_key=True)`
- NEVER use the old `Column(Integer, primary_key=True)` syntax.
## Querying
- NEVER use `session.query()`. This is legacy 1.x syntax.
- ALWAYS use `sqlalchemy.select()` combined with `await session.execute(stmt)`.
- Extract results using `.scalars().all()` or `.scalar_one_or_none()`.
## Session Management
- Use `sqlalchemy.ext.asyncio.AsyncSession`.
- Ensure all database I/O is awaited.
+30
View File
@@ -0,0 +1,30 @@
---
name: strict-mypy
description: Enforces strict MyPy type checking conventions matching the project's pyproject.toml configuration. Use whenever writing or modifying Python code.
---
# Strict MyPy Typing Standards
This project uses a highly strict MyPy configuration (`strict = true`). When writing or refactoring Python code, you MUST adhere to the following rules to ensure the code passes type checking:
## 1. Absolute Signature Completeness
- EVERY function and method must have fully typed arguments and a return type (`disallow_untyped_defs`, `disallow_incomplete_defs`).
- ALWAYS specify `-> None` for functions and methods that do not return a value.
- NEVER leave type hints partially defined (e.g., typing only some arguments).
- Decorators must be completely type-hinted using `typing.Callable` or `typing.ParamSpec` (`disallow_untyped_decorators`).
## 2. No Implicit Optionals
- `arg: str = None` is strictly forbidden.
- You MUST explicitly union with None: `arg: str | None = None` (`no_implicit_optional`).
## 3. Strict Generics
- NEVER use bare collections as types.
- ALWAYS specify the inner types: use `list[str]`, `dict[str, int]`, or `tuple[int, ...]` instead of bare `list`, `dict`, or `tuple` (`disallow_any_generics`).
## 4. Restrictions on `Any` and Subclassing
- Do not subclass from untyped external libraries or `Any` (`disallow_subclassing_any`).
- Avoid using external types that have no stubs and resolve to `Any` (`disallow_any_unimported`).
- Refrain from returning `Any` to avoid contaminating the type inference of calling functions (`warn_return_any`).
## 5. Pydantic Strictness
- `pydantic.mypy` is active with strict initialization.
- When instantiating Pydantic models, ensure you are passing exactly the expected typed arguments, as extra or untyped arguments will fail type checks (`init_typed = true`, `init_forbid_extra = true`).
+1 -1
View File
@@ -3,7 +3,7 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.15.2 rev: v0.16.4
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@@ -0,0 +1,62 @@
"""recipe steps photos
Revision ID: 09a4f0ca450f
Revises: 82af4dcd0b42
Create Date: 2026-03-01 06:22:29.832426
"""
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision = "09a4f0ca450f"
down_revision = "82af4dcd0b42"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.create_table(
"recipe_steps",
sa.Column("id", sa.Uuid(), server_default=sa.text("gen_random_uuid()"), nullable=False),
sa.Column("recipe_id", sa.Uuid(), nullable=False),
sa.Column("order_index", sa.Integer(), nullable=False),
sa.Column("phase", sa.Text(), nullable=False),
sa.Column("title", sa.Text(), nullable=False),
sa.Column("text", sa.Text(), nullable=False),
sa.Column("duration_minutes", sa.Integer(), nullable=True),
sa.Column("notes", sa.Text(), nullable=True),
sa.ForeignKeyConstraint(["recipe_id"], ["recipes.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_recipe_steps_recipe_id"), "recipe_steps", ["recipe_id"], unique=False)
op.create_index(op.f("ix_recipe_steps_order_index"), "recipe_steps", ["order_index"], unique=False)
op.create_table(
"recipe_photos",
sa.Column("id", sa.Uuid(), server_default=sa.text("gen_random_uuid()"), nullable=False),
sa.Column("recipe_id", sa.Uuid(), nullable=False),
sa.Column("image_id", sa.Uuid(), nullable=False),
sa.Column("order_index", sa.Integer(), nullable=False),
sa.Column("caption", sa.Text(), nullable=False, server_default=""),
sa.ForeignKeyConstraint(["image_id"], ["images.id"], ondelete="CASCADE"),
sa.ForeignKeyConstraint(["recipe_id"], ["recipes.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_recipe_photos_recipe_id"), "recipe_photos", ["recipe_id"], unique=False)
op.create_index(op.f("ix_recipe_photos_image_id"), "recipe_photos", ["image_id"], unique=False)
op.create_index(op.f("ix_recipe_photos_order_index"), "recipe_photos", ["order_index"], unique=False)
def downgrade() -> None:
op.drop_index(op.f("ix_recipe_photos_order_index"), table_name="recipe_photos")
op.drop_index(op.f("ix_recipe_photos_image_id"), table_name="recipe_photos")
op.drop_index(op.f("ix_recipe_photos_recipe_id"), table_name="recipe_photos")
op.drop_table("recipe_photos")
op.drop_index(op.f("ix_recipe_steps_order_index"), table_name="recipe_steps")
op.drop_index(op.f("ix_recipe_steps_recipe_id"), table_name="recipe_steps")
op.drop_table("recipe_steps")
+4 -2
View File
@@ -17,7 +17,6 @@ from ..models.incentive import Incentive
from ..models.inventory import Inventory from ..models.inventory import Inventory
from ..models.journal import Journal from ..models.journal import Journal
from ..models.login_history import LoginHistory from ..models.login_history import LoginHistory
from ..models.mozimo_stock_register import MozimoStockRegister
from ..models.period import Period from ..models.period import Period
from ..models.permission import Permission from ..models.permission import Permission
from ..models.price import Price from ..models.price import Price
@@ -28,6 +27,8 @@ from ..models.rate_contract import RateContract
from ..models.rate_contract_item import RateContractItem from ..models.rate_contract_item import RateContractItem
from ..models.recipe import Recipe from ..models.recipe import Recipe
from ..models.recipe_item import RecipeItem from ..models.recipe_item import RecipeItem
from ..models.recipe_photo import RecipePhoto
from ..models.recipe_step import RecipeStep
from ..models.recipe_tag import RecipeTag from ..models.recipe_tag import RecipeTag
from ..models.recipe_template import RecipeTemplate from ..models.recipe_template import RecipeTemplate
from ..models.role import Role from ..models.role import Role
@@ -62,7 +63,6 @@ __all__ = [
"Inventory", "Inventory",
"Journal", "Journal",
"LoginHistory", "LoginHistory",
"MozimoStockRegister",
"Permission", "Permission",
"Period", "Period",
"Price", "Price",
@@ -73,6 +73,8 @@ __all__ = [
"RateContractItem", "RateContractItem",
"Recipe", "Recipe",
"RecipeItem", "RecipeItem",
"RecipePhoto",
"RecipeStep",
"RecipeTag", "RecipeTag",
"RecipeTemplate", "RecipeTemplate",
"reg", "reg",
+4 -4
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
import random import secrets
import string import string
import uuid import uuid
@@ -49,9 +49,9 @@ class Client:
@classmethod @classmethod
def create(cls, db: Session) -> Client: def create(cls, db: Session) -> Client:
client_code = random.randint(1000, 9999) client_code = secrets.randbelow(9000) + 1000
otp = random.randint(1000, 9999) otp = secrets.randbelow(9000) + 1000
name = "".join(random.choice(string.ascii_uppercase + string.digits) for x in range(6)) name = "".join(secrets.choice(string.ascii_uppercase + string.digits) for x in range(6))
client = Client(client_code, name, False, otp) client = Client(client_code, name, False, otp)
db.add(client) db.add(client)
return client return client
+24 -14
View File
@@ -6,7 +6,7 @@ from datetime import date
from decimal import Decimal from decimal import Decimal
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sqlalchemy import Date, ForeignKey, Numeric, Text, UniqueConstraint, Uuid, text from sqlalchemy import Date, ForeignKey, Numeric, Text, Uuid, text
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg from ..db.base_class import reg
@@ -15,6 +15,8 @@ from .recipe_tag import RecipeTag
if TYPE_CHECKING: if TYPE_CHECKING:
from .recipe_item import RecipeItem from .recipe_item import RecipeItem
from .recipe_photo import RecipePhoto
from .recipe_step import RecipeStep
from .stock_keeping_unit import StockKeepingUnit from .stock_keeping_unit import StockKeepingUnit
from .tag import Tag from .tag import Tag
@@ -22,22 +24,34 @@ if TYPE_CHECKING:
@reg.mapped_as_dataclass(unsafe_hash=True) @reg.mapped_as_dataclass(unsafe_hash=True)
class Recipe: class Recipe:
__tablename__ = "recipes" __tablename__ = "recipes"
__table_args__ = (UniqueConstraint("sku_id", "date"),)
id: Mapped[uuid.UUID] = mapped_column( id: Mapped[uuid.UUID] = mapped_column(
Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()") Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()")
) )
sku_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("stock_keeping_units.id"), nullable=False)
date_: Mapped[date] = mapped_column("date", Date, nullable=False, index=True) date_: Mapped[date] = mapped_column("date", Date(), nullable=False)
source: Mapped[str] = mapped_column(Text, nullable=False) source: Mapped[str] = mapped_column(Text, nullable=False)
instructions: Mapped[str] = mapped_column(Text, nullable=False) instructions: Mapped[str] = mapped_column(Text, nullable=False) # legacy single-text instructions
garnishing: Mapped[str] = mapped_column(Text, nullable=False) garnishing: Mapped[str] = mapped_column(Text, nullable=False)
plating: Mapped[str] = mapped_column(Text, nullable=False) plating: Mapped[str] = mapped_column(Text, nullable=False)
notes: Mapped[str] = mapped_column(Text, nullable=False) notes: Mapped[str] = mapped_column(Text, nullable=False)
sku_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("stock_keeping_units.id"), nullable=False)
recipe_yield: Mapped[Decimal] = mapped_column(Numeric(precision=15, scale=2), nullable=False) recipe_yield: Mapped[Decimal] = mapped_column(Numeric(12, 4), nullable=False)
items: Mapped[list[RecipeItem]] = relationship("RecipeItem", back_populates="recipe") items: Mapped[list[RecipeItem]] = relationship("RecipeItem", back_populates="recipe")
steps: Mapped[list[RecipeStep]] = relationship(
"RecipeStep",
back_populates="recipe",
cascade="all, delete-orphan",
order_by="RecipeStep.order_index",
)
photos: Mapped[list[RecipePhoto]] = relationship(
"RecipePhoto",
back_populates="recipe",
cascade="all, delete-orphan",
order_by="RecipePhoto.order_index",
)
sku: Mapped[StockKeepingUnit] = relationship("StockKeepingUnit", back_populates="recipes") sku: Mapped[StockKeepingUnit] = relationship("StockKeepingUnit", back_populates="recipes")
tags: Mapped[list[Tag]] = relationship( tags: Mapped[list[Tag]] = relationship(
"Tag", "Tag",
@@ -54,21 +68,17 @@ class Recipe:
garnishing: str, garnishing: str,
plating: str, plating: str,
notes: str, notes: str,
sku: StockKeepingUnit,
recipe_yield: Decimal, recipe_yield: Decimal,
sku_id: uuid.UUID | None = None,
sku: StockKeepingUnit | None = None,
id_: uuid.UUID | None = None, id_: uuid.UUID | None = None,
): ) -> None:
self.date_ = date_ self.date_ = date_
self.source = source self.source = source
self.instructions = instructions self.instructions = instructions
self.garnishing = garnishing self.garnishing = garnishing
self.plating = plating self.plating = plating
self.notes = notes self.notes = notes
self.sku = sku
self.recipe_yield = recipe_yield self.recipe_yield = recipe_yield
if sku_id is not None:
self.sku_id = sku_id
if sku is not None:
self.sku = sku
if id_ is not None: if id_ is not None:
self.id = id_ self.id = id_
+49
View File
@@ -0,0 +1,49 @@
from __future__ import annotations
import uuid
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, Integer, Text, Uuid, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg
if TYPE_CHECKING:
from .db_image import DbImage
from .recipe import Recipe
@reg.mapped_as_dataclass(unsafe_hash=True)
class RecipePhoto:
"""Links recipes to DbImage with caption + ordering."""
__tablename__ = "recipe_photos"
id: Mapped[uuid.UUID] = mapped_column(
Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()")
)
recipe_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("recipes.id"), nullable=False, index=True)
image_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("images.id"), nullable=False, index=True)
order_index: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
caption: Mapped[str] = mapped_column(Text, nullable=False)
recipe: Mapped[Recipe] = relationship("Recipe", back_populates="photos")
image: Mapped[DbImage] = relationship("DbImage")
def __init__(
self,
recipe_id: uuid.UUID,
image_id: uuid.UUID,
order_index: int,
caption: str = "",
id_: uuid.UUID | None = None,
) -> None:
self.recipe_id = recipe_id
self.image_id = image_id
self.order_index = order_index
self.caption = caption
if id_ is not None:
self.id = id_
+59
View File
@@ -0,0 +1,59 @@
from __future__ import annotations
import uuid
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, Integer, Text, Uuid, text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ..db.base_class import reg
if TYPE_CHECKING:
from .recipe import Recipe
@reg.mapped_as_dataclass(unsafe_hash=True)
class RecipeStep:
"""Structured recipe steps.
Stored separately from legacy Recipe.instructions (single text field).
"""
__tablename__ = "recipe_steps"
id: Mapped[uuid.UUID] = mapped_column(
Uuid, primary_key=True, insert_default=uuid.uuid4, server_default=text("gen_random_uuid()")
)
recipe_id: Mapped[uuid.UUID] = mapped_column(Uuid, ForeignKey("recipes.id"), nullable=False, index=True)
order_index: Mapped[int] = mapped_column(Integer, nullable=False, index=True)
phase: Mapped[str] = mapped_column(Text, nullable=False) # Prep/Cook/Finish/Serve
title: Mapped[str] = mapped_column(Text, nullable=False)
text: Mapped[str] = mapped_column(Text, nullable=False)
duration_minutes: Mapped[int | None] = mapped_column(Integer, nullable=True)
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
recipe: Mapped[Recipe] = relationship("Recipe", back_populates="steps")
def __init__(
self,
recipe_id: uuid.UUID,
order_index: int,
phase: str,
title: str,
text: str,
duration_minutes: int | None = None,
notes: str | None = None,
id_: uuid.UUID | None = None,
) -> None:
self.recipe_id = recipe_id
self.order_index = order_index
self.phase = phase
self.title = title
self.text = text
self.duration_minutes = duration_minutes
self.notes = notes
if id_ is not None:
self.id = id_
File diff suppressed because it is too large. Load diff
+18 -9
View File
@@ -3,7 +3,7 @@ from datetime import date, datetime, time, timedelta
from decimal import Decimal from decimal import Decimal
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Depends, Security from fastapi import APIRouter, Depends, Query, Security
from sqlalchemy import desc, not_, or_, select from sqlalchemy import desc, not_, or_, select
from sqlalchemy.orm import Session, contains_eager from sqlalchemy.orm import Session, contains_eager
from sqlalchemy.sql.functions import count from sqlalchemy.sql.functions import count
@@ -88,15 +88,24 @@ def report_data(
finish_date: Annotated[date, Depends(report_finish_date)], finish_date: Annotated[date, Depends(report_finish_date)],
user: Annotated[UserToken, Security(get_user, scopes=["ledger"])], user: Annotated[UserToken, Security(get_user, scopes=["ledger"])],
db: SessionDep, db: SessionDep,
p: bool | None = None, # Posted posted: Annotated[bool | None, Query(alias="p")] = None, # Posted
ps: int = 50, # Page Size page_size: Annotated[int, Query(alias="ps")] = 50, # Page Size
pi: int = 0, # Page Index page_index: Annotated[int, Query(alias="pi")] = 0, # Page Index
a: str = "date", # Active Sort active_sort: Annotated[str, Query(alias="a")] = "date", # Active Sort
d: str = "desc", # Sort Direction sort_direction: Annotated[str, Query(alias="d")] = "desc", # Sort Direction
i: bool = False, # Show issue vouchers show_issue_vouchers: Annotated[bool, Query(alias="i")] = False, # Show issue vouchers
) -> schemas.Report: ) -> schemas.Report:
assert i is not None and ps is not None and pi is not None and a is not None and d is not None counts, report = build_report(
counts, report = build_report(start_date, finish_date, p, i, ps, pi, a, d, db) start_date,
finish_date,
posted,
show_issue_vouchers,
page_size,
page_index,
active_sort,
sort_direction,
db,
)
return schemas.Report( return schemas.Report(
counts=counts, counts=counts,
report=[ report=[
+2 -3
View File
@@ -18,7 +18,6 @@ from ..models.product_group import ProductGroup
from ..models.product_version import ProductVersion from ..models.product_version import ProductVersion
from ..models.sku_version import SkuVersion from ..models.sku_version import SkuVersion
from ..models.stock_keeping_unit import StockKeepingUnit from ..models.stock_keeping_unit import StockKeepingUnit
from ..schemas.account import AccountLink
from ..schemas.product_group import ProductGroupLink from ..schemas.product_group import ProductGroupLink
from ..schemas.temporal_product import Product as ProductModel from ..schemas.temporal_product import Product as ProductModel
from ..schemas.temporal_product import StockKeepingUnit as SkuModel from ..schemas.temporal_product import StockKeepingUnit as SkuModel
@@ -52,7 +51,7 @@ def update_route(
product_v.description = data_version.description product_v.description = data_version.description
product_v.fraction_units = data_version.fraction_units product_v.fraction_units = data_version.fraction_units
product_v.product_group_id = data_version.product_group.id_ product_v.product_group_id = data_version.product_group.id_
product_v.account_id = Account.all_purchases() # product_v.account_id = Account.all_purchases()
product_v.is_purchased = data_version.is_purchased product_v.is_purchased = data_version.is_purchased
product_v.is_sold = data_version.is_sold product_v.is_sold = data_version.is_sold
product_v.allergen = data_version.allergen product_v.allergen = data_version.allergen
@@ -225,7 +224,7 @@ def product_info(product: Product) -> TemporalProduct:
is_purchased=version.is_purchased, is_purchased=version.is_purchased,
is_sold=version.is_sold, is_sold=version.is_sold,
product_group=ProductGroupLink(id_=version.product_group.id, name=version.product_group.name), product_group=ProductGroupLink(id_=version.product_group.id, name=version.product_group.name),
account=AccountLink(id_=version.account.id, name=version.account.name), # account=AccountLink(id_=version.account.id, name=version.account.name),
allergen=version.allergen, allergen=version.allergen,
protein=version.protein, protein=version.protein,
carbohydrate=version.carbohydrate, carbohydrate=version.carbohydrate,
+33 -3
View File
@@ -2,9 +2,12 @@ import uuid
from datetime import date, datetime from datetime import date, datetime
from fastapi import Form
from pydantic import ( from pydantic import (
AliasChoices,
BaseModel, BaseModel,
ConfigDict, ConfigDict,
Field,
FieldSerializationInfo, FieldSerializationInfo,
field_serializer, field_serializer,
field_validator, field_validator,
@@ -13,24 +16,37 @@ from pydantic import (
from . import Daf, to_camel from . import Daf, to_camel
from .product import ProductLink from .product import ProductLink
from .recipe_item import RecipeItem from .recipe_item import RecipeItem
from .recipe_photo import RecipePhoto
from .recipe_step import RecipeStep
class RecipeIn(BaseModel): class RecipeIn(BaseModel):
sku: ProductLink
date_: date date_: date
source: str source: str
instructions: str instructions: str
garnishing: str garnishing: str
plating: str plating: str
sku: ProductLink
recipe_yield: Daf recipe_yield: Daf
notes: str notes: str
items: list[RecipeItem] # New structured content
steps: list[RecipeStep] = Field(default_factory=list)
photos: list[RecipePhoto] = Field(default_factory=list)
# Ingredients (renamed from legacy "items")
# Accept both keys for backward compatibility, serialize as "ingredients".
ingredients: list[RecipeItem] = Field(
default_factory=list,
validation_alias=AliasChoices("ingredients", "items"),
serialization_alias="ingredients",
)
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True) model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@field_validator("date_", mode="before") @field_validator("date_", mode="before")
@classmethod @classmethod
def parse_start_date(cls, value: date | str) -> date: def parse_date(cls, value: date | str) -> date:
if isinstance(value, date): if isinstance(value, date):
return value return value
return datetime.strptime(value, "%d-%b-%Y").date() return datetime.strptime(value, "%d-%b-%Y").date()
@@ -39,11 +55,25 @@ class RecipeIn(BaseModel):
def serialize_date(self, value: date, info: FieldSerializationInfo) -> str: def serialize_date(self, value: date, info: FieldSerializationInfo) -> str:
return value.strftime("%d-%b-%Y") return value.strftime("%d-%b-%Y")
@classmethod
def load_form(cls, data: str = Form(...)) -> BaseModel:
import json
json_data = json.loads(data)
# pydantic v2
return cls.model_validate(json_data)
class Recipe(RecipeIn): class Recipe(RecipeIn):
id_: uuid.UUID id_: uuid.UUID
product_group_id: uuid.UUID | None = None product_group_id: uuid.UUID | None = None
units: str | None = None units: str | None = None
# Derived metrics (for list view)
step_count: int | None = None
photo_count: int | None = None
total_duration_minutes: int | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True) model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
+29
View File
@@ -0,0 +1,29 @@
import uuid
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
class RecipePhotoIn(BaseModel):
"""Photo link + metadata.
For input:
- id_ is optional (existing image id)
- resized / thumbnail are base64 strings (may include data URL prefix)
For output:
- resized / thumbnail are URLs to /db-image/{id}/...
"""
id_: uuid.UUID | None = None
order_index: int = Field(0, ge=0)
caption: str = ""
resized: str | None = None
thumbnail: str | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
class RecipePhoto(RecipePhotoIn):
# kept for symmetry / future extensions
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
+21
View File
@@ -0,0 +1,21 @@
import uuid
from pydantic import BaseModel, ConfigDict, Field
from . import to_camel
class RecipeStepIn(BaseModel):
order_index: int = Field(0, ge=0)
phase: str
title: str
text: str
duration_minutes: int | None = Field(default=None, ge=0)
notes: str | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
class RecipeStep(RecipeStepIn):
id_: uuid.UUID | None = None
model_config = ConfigDict(str_strip_whitespace=True, alias_generator=to_camel, populate_by_name=True)
@@ -7,7 +7,6 @@ from typing import Annotated
from pydantic import BaseModel, ConfigDict, Field, SerializationInfo, field_serializer, field_validator from pydantic import BaseModel, ConfigDict, Field, SerializationInfo, field_serializer, field_validator
from . import Daf, to_camel from . import Daf, to_camel
from .account import AccountLink
from .product_group import ProductGroupLink from .product_group import ProductGroupLink
@@ -20,7 +19,6 @@ class Product(BaseModel):
description: str | None description: str | None
fraction_units: Annotated[str, Field(min_length=1)] fraction_units: Annotated[str, Field(min_length=1)]
product_group: ProductGroupLink = Field(...) product_group: ProductGroupLink = Field(...)
account: AccountLink
is_purchased: bool is_purchased: bool
is_sold: bool is_sold: bool
+19 -20
View File
@@ -5,33 +5,34 @@ description = "Accounting plus inventory management for a restaurant."
requires-python = ">=3.14" requires-python = ">=3.14"
authors = [{ name = "tanshu", email = "git@tanshu.com" }] authors = [{ name = "tanshu", email = "git@tanshu.com" }]
dependencies = [ dependencies = [
"alembic>=1.18.4", "alembic>=1.19.1",
"arq>=0.28.0", "arq>=0.28.0",
"cryptography>=48.0.0", "cryptography>=50.0.0",
"fastapi[all]>=0.136.1", "fastapi[all]>=0.141.1",
"gunicorn>=26.0.0", "gunicorn>=26.2.0",
"itsdangerous>=2.2.0", "itsdangerous>=2.2.0",
"openpyxl>=3.1.5", "openpyxl>=3.1.5",
"pandas>=3.0.2", "pandas>=3.0.5",
"passlib[bcrypt]>=1.7.4", "passlib[bcrypt]>=1.7.4",
"psycopg[binary,pool]>=3.3.4", "psycopg[binary,pool]>=3.3.4",
"pydantic>=2.13.3", "pydantic>=2.13.4",
"pydantic-settings>=2.14.0", "pydantic-settings>=2.15.0",
"pyjwt>=2.12.1", "pyjwt>=2.13.0",
"python-dotenv>=1.2.2", "python-dotenv>=1.2.3",
"python-multipart>=0.0.27", "python-multipart>=0.0.32",
"sqlalchemy>=2.0.49", "sqlalchemy>=2.0.52",
"uvicorn[standard]>=0.46.0", "uvicorn[standard]>=0.52.4",
] ]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"bandit>=1.9.4", "bandit>=1.9.4",
"mypy>=1.20.2", "mypy>=2.3.1",
"pre-commit>=4.6.0", "pandas-stubs>=3.0.5.260730",
"ruff>=0.15.12", "pre-commit>=4.6.2",
"safety>=3.7.0", "ruff>=0.16.4",
"types-openpyxl>=3.1.5.20260408", "safety>=3.8.1",
"types-openpyxl>=3.1.5.20260807",
] ]
[build-system] [build-system]
@@ -107,9 +108,7 @@ ignore = [
"W191", # indentation contains tabs "W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException "B904", # Allow raising exceptions without from e, for HTTPException
] ]
fixable = [ fixable = ["ALL"] # Allow fix for all enabled rules (when `--fix`) is provided.
"ALL"
] # Allow fix for all enabled rules (when `--fix`) is provided.
dummy-variable-rgx = "^(user|_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" dummy-variable-rgx = "^(user|_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
+377 -285
View File
@@ -12,16 +12,16 @@ resolution-markers = [
[[package]] [[package]]
name = "alembic" name = "alembic"
version = "1.18.4" version = "1.19.1"
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/16/2b/e4153978368de59918115c9e01d3ebf58a558a7285efa7e960c383c4b59a/alembic-1.19.1.tar.gz", hash = "sha256:e0fca0518118c78acc493e31bcb5402f190057aaf6df8b5b95ce94c4789cf648", size = 2070816, upload-time = "2026-08-08T16:32:01.565Z" }
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/20/89/e62cc37b69ad357cc8ecd6e7367f5245f523d3cbb338a66197212bdf6749/alembic-1.19.1-py3-none-any.whl", hash = "sha256:b39018cb3d9413a19cbd54cf3c02ad33998641f0538eb77413a488a21c3e14be", size = 265946, upload-time = "2026-08-08T16:32:03.153Z" },
] ]
[[package]] [[package]]
@@ -67,6 +67,70 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/32/66b616976c5058d434ca2017979bfffd784888177b16b5038abcec93954a/arq-0.28.0-py3-none-any.whl", hash = "sha256:b1696bf5614d60f4172a2c0cbdc177e23ba03a5eb9acc29bd8181f4ea71fff94", size = 26061, upload-time = "2026-04-16T10:50:22.321Z" }, { url = "https://files.pythonhosted.org/packages/48/32/66b616976c5058d434ca2017979bfffd784888177b16b5038abcec93954a/arq-0.28.0-py3-none-any.whl", hash = "sha256:b1696bf5614d60f4172a2c0cbdc177e23ba03a5eb9acc29bd8181f4ea71fff94", size = 26061, upload-time = "2026-04-16T10:50:22.321Z" },
] ]
[[package]]
name = "ast-serialize"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/a9/11851c3e02a3fea2ddc9932d1fdc7d2edaeecc0d2e11bc5f2a7fde2b0934/ast_serialize-0.8.0.tar.gz", hash = "sha256:6c37c43e4004dfb42d321ddedc569dc17ff4259296f3af577c9ea46a809bc010", size = 845638, upload-time = "2026-08-07T11:29:02.152Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/34/16/6e520b57cd8c75914b38c670ad4593d13c22911e4306cc7165dab8b0789b/ast_serialize-0.8.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3d822605fa7bb326ef868d25fafced7fc660fa46d9b90c02ea86d5e2f5d325f7", size = 863924, upload-time = "2026-08-07T11:27:34.579Z" },
{ url = "https://files.pythonhosted.org/packages/03/e1/48802de9b22a2bcad42ec80601a17e3f69172fe4f590e6311bcc2b323aeb/ast_serialize-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:2efa40b068197d5efb62655b43baadb842ed71c4958cccd3e8b86a35726f0119", size = 1177662, upload-time = "2026-08-07T11:27:36.196Z" },
{ url = "https://files.pythonhosted.org/packages/38/d4/323438db76bded3a1f3523a3167b8325916b2ddceb2107a330c6ec9fcf4d/ast_serialize-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:db1b957291bca08c7e72f43a12357b2948e20775d970e3fc3dac0aa3160ab725", size = 1167072, upload-time = "2026-08-07T11:27:37.646Z" },
{ url = "https://files.pythonhosted.org/packages/77/82/53c5400b54144b56de8ed7f957fd1ccd97e42482009292ab46121d15f8dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdc0d5b18ff8fb364e87923e47c0a91d0d69dbcaeaa274591f7fd26892cc3a3a", size = 1225497, upload-time = "2026-08-07T11:27:39.225Z" },
{ url = "https://files.pythonhosted.org/packages/44/5f/36c07327a8b91303fbf1382c7c3e8a2902072dbe1b9546138a5288e75ff0/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9da7330f3e235bf7da89b8d39205c6350fc0c08a85379743f2df9fff87d6d980", size = 1227101, upload-time = "2026-08-07T11:27:40.799Z" },
{ url = "https://files.pythonhosted.org/packages/9d/48/5adf5c67addc7ddb328122208c6d375a84cf154984f412b4087330a157bd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3186969ee66a9863b00acc6523ace44c56974eecb348a7ea4b228d9f0b80e19", size = 1424001, upload-time = "2026-08-07T11:27:42.708Z" },
{ url = "https://files.pythonhosted.org/packages/38/a1/70074dd3869d2b0e934f91891d8d6b734361cd3b80f85ca7ece2e668ecdd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40a57b73731be45da4fa41430c4d5dc94a24b3a4faba7b9e069978c0402064ea", size = 1245545, upload-time = "2026-08-07T11:27:44.4Z" },
{ url = "https://files.pythonhosted.org/packages/e3/be/53b9c0a8a6399950c2e3546bdfab96d2b299d5b114b47eb94fd3c49c4054/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5075b9da3ef807eda752502446dfecea3b381c4900b7e27a5d5f4f899eb39951", size = 1248961, upload-time = "2026-08-07T11:27:45.781Z" },
{ url = "https://files.pythonhosted.org/packages/eb/13/3651d3812548a2bda15e26e5dd51aadb48cf682d0865370255fcf0e367dd/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:293cc1c5bfa741f8e3fbe8175b9c07beee487c9a6fdbb25a5acad9f1df2d30a9", size = 1243877, upload-time = "2026-08-07T11:27:47.325Z" },
{ url = "https://files.pythonhosted.org/packages/21/a0/521f0bf000f675e9312a4aae2c8ba7a992405d072a85c485e08fd59433b9/ast_serialize-0.8.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0910c3442a75216dde0f102d854ba2aaa71d2482e0ee213630b9bf29584fba3", size = 1293903, upload-time = "2026-08-07T11:27:49.264Z" },
{ url = "https://files.pythonhosted.org/packages/b1/7e/402fc902568aa2ee65865a3e151f000db0153da8ce6b1be4c9c349025f8d/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:43dd6d596879bb1cb8a12cc9dae7bb10090a39a35883026c24f82488a195619a", size = 1401070, upload-time = "2026-08-07T11:27:50.947Z" },
{ url = "https://files.pythonhosted.org/packages/ff/7c/97d4b66c057f1706fc8be6dd532cc77c988794357c8f4ffdb6adabb39562/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8c9d537f59e936392cfd3597789d1390304dd659efc3c486ce7f40fb6b8a9f53", size = 1502602, upload-time = "2026-08-07T11:27:52.364Z" },
{ url = "https://files.pythonhosted.org/packages/89/6f/72cc3b71562001bba46e898ccfbf1844f7939b3e28912736206102f2e5a8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f0190a33d7f97c65e9069f7a7f40499eea6b5cbe260c558378109caf20ce934b", size = 1495848, upload-time = "2026-08-07T11:27:53.803Z" },
{ url = "https://files.pythonhosted.org/packages/a0/53/d6f629d1e49308b2f363dae028baa213ec222c9106fa1f7f0d1f7b41499a/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:77308ae6c5cf5264cc0f01a7c556ec77a9e68eb1f61b093534d698139fdc3b14", size = 1556556, upload-time = "2026-08-07T11:27:55.342Z" },
{ url = "https://files.pythonhosted.org/packages/ee/22/340f35dd8dfc6d412d53dc20699ca014b8d228db923e8ed4759c512b162c/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8d53a23f27e1ed3a36b2d26fd2a1a6228c8e85a1ed62ff7cdb44bd610769f20a", size = 1417822, upload-time = "2026-08-07T11:27:56.712Z" },
{ url = "https://files.pythonhosted.org/packages/11/29/6dde5c13fbebc051d3a6df4ec0a6fd1d5359333cc1193f7f609f3410b4d8/ast_serialize-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa5e7cb08f96fed9121f77b224151e41caf88feab9d652bb46c78202b6fbeda", size = 1445153, upload-time = "2026-08-07T11:27:58.275Z" },
{ url = "https://files.pythonhosted.org/packages/62/c5/f473a8ed030f7a0ca24b9849cca184677a50c053867a7b808c2e1289bbd3/ast_serialize-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:fa70ed4dea0bb18b30a1789c77baa701d0ef30c474f2ccabdea61e25623a8827", size = 1063711, upload-time = "2026-08-07T11:27:59.793Z" },
{ url = "https://files.pythonhosted.org/packages/23/63/39e171fcd38ca057c2e1979d5ee81ac7a3502784abe3d83df7454f7a0978/ast_serialize-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d8b3c8eee4c1baef9d4e84d2a59a805501617127be42615cb48970b15b0892b6", size = 1103740, upload-time = "2026-08-07T11:28:01.405Z" },
{ url = "https://files.pythonhosted.org/packages/21/1c/d00762b399e7726d68d0a088cc946e3a4c60f1c6176f557608f672f627f3/ast_serialize-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:ac4f0a83c55a9b782f79ad55a5247b7db123c1db405959791c2ef886e9710c9f", size = 1076021, upload-time = "2026-08-07T11:28:02.947Z" },
{ url = "https://files.pythonhosted.org/packages/4c/11/911210c3c78923273a9211a2b6cfc4c8aa723b30dab3e1c8d19afb983b40/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:86b8a1e6d90467345356098b040150e82fbc26d24a7a202224b13dc1f6264ca0", size = 1177715, upload-time = "2026-08-07T11:28:04.654Z" },
{ url = "https://files.pythonhosted.org/packages/77/89/6282881c8587606638db153cbe21e1e0c4d1f3970dee1aa0610a1c62a026/ast_serialize-0.8.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:39e92ff8e8cb45947fe9007174b2950e1fb098e6abd00266a13cd3bcf6675068", size = 1169347, upload-time = "2026-08-07T11:28:06.1Z" },
{ url = "https://files.pythonhosted.org/packages/97/78/a9f846a03a340ff3728c915f23338ca742742f3292700559cdb3ad999b1e/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85d8d18db5b2dfcb3b7e38a4d600ca35504c0ed8a6f75cd1c811e4ffe248a15", size = 1225916, upload-time = "2026-08-07T11:28:07.654Z" },
{ url = "https://files.pythonhosted.org/packages/c0/15/aba6ef8a988a6eceb6f0359589aac509e29ae2dba67fd9bfd5af0c3f13e7/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9830ff7e764f74d9eefb01170c61a9f0fd2c027dac5fcb72e064decd57d56371", size = 1227135, upload-time = "2026-08-07T11:28:09.504Z" },
{ url = "https://files.pythonhosted.org/packages/94/29/3f63d696ea7c5b8abadcecc3505be51bd900daaccc522ed8322fa5b05a93/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6479d9722a4cd21b578f5478074c41e6169f04811996ec881655560f703a5bba", size = 1425040, upload-time = "2026-08-07T11:28:11.044Z" },
{ url = "https://files.pythonhosted.org/packages/e2/5d/0aac338604ff59df5774d4304307898982252f325ff7cafe31d52fedcb65/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a63bed264e818cd83eec11feed0f50aa162542b91132ef58afebc857182763a5", size = 1246278, upload-time = "2026-08-07T11:28:12.519Z" },
{ url = "https://files.pythonhosted.org/packages/23/ca/9f1ef795bb724719532bd86dbec11e5b66857d3fbe9b6772baec0191a6ed/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d187197d234aa45d6cfa2b096be5f666e8cc2e7eb3722d0ab8926293cf5720c", size = 1250029, upload-time = "2026-08-07T11:28:13.896Z" },
{ url = "https://files.pythonhosted.org/packages/dc/25/5e061372d2ed953b9ba3b9c4f73de3b8e9234cda3f6c088db4686801d0e1/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:2d39a56282cfcc0d8eeea37267c754be59c98d48505c23b1dae5c6011f3813dd", size = 1243575, upload-time = "2026-08-07T11:28:15.37Z" },
{ url = "https://files.pythonhosted.org/packages/a8/c1/ae7da218053120635a4ca802366c69f707203641af95372eeb83f70dfd52/ast_serialize-0.8.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7cc5f10386994c0f4844f1e6d6a97127e9b478660eb6dec2b257644f0acab64", size = 1294396, upload-time = "2026-08-07T11:28:16.813Z" },
{ url = "https://files.pythonhosted.org/packages/2e/89/271d1f49c5269fcddcc789ea3f25be401f6723fc1138aeda539f4d05516d/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6102f2f985c2e542be85cd857678ec9356fefa792b93cadfadd31139f5696f27", size = 1401987, upload-time = "2026-08-07T11:28:18.333Z" },
{ url = "https://files.pythonhosted.org/packages/55/be/4e7d77fcf571ac7cb5cf7115a20c36642bd7d29473b45dfaaefeb9618f90/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:3a8660fe66667b76a6e9dccd1d33e66b229fde3b308db991c041609226c005b6", size = 1502904, upload-time = "2026-08-07T11:28:20.039Z" },
{ url = "https://files.pythonhosted.org/packages/8b/ae/ed1de2db7e019d4236fbc164ffa5ef9a6022a300a342bbf142d21b7c141e/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:e7266307e5fba39836edb79def8608887af48820508bff3c5f2941e1e04d1534", size = 1496967, upload-time = "2026-08-07T11:28:21.734Z" },
{ url = "https://files.pythonhosted.org/packages/92/89/5fea507fae5c5f18b7dc7f95e5c00956574b8c717b8fd2049c504fab0b18/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca7e6fd1ad845d1cc649dc2ecd499db2f8f46af5bf8da7b70dd858774cc038b", size = 1559041, upload-time = "2026-08-07T11:28:23.194Z" },
{ url = "https://files.pythonhosted.org/packages/42/71/478d69df21b64e064554a68134c94be304270316ca676a94e63c389a636a/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:2880350b13d3eae69a0d70bc1fb6c9bfaca4dbd0e20ba8cd1aa483080b56ff06", size = 1417367, upload-time = "2026-08-07T11:28:24.601Z" },
{ url = "https://files.pythonhosted.org/packages/5e/2d/8962dc8d5b3a9dc27b36f9db199afa25264c741505469d9ec10ffbfd2ba7/ast_serialize-0.8.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:ab0f9a59f7d63d0d441b56b9a818b273705264352d5115cfee12e940e816d958", size = 1446178, upload-time = "2026-08-07T11:28:26.152Z" },
{ url = "https://files.pythonhosted.org/packages/4f/22/14d2ad4fd1d1bcd0dc687ca268e0630069f45162496260c0efb70ee0ea72/ast_serialize-0.8.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:0485a25ef519c62e749ee3c1ad8070e591b380d67226349eb5a70b228dc1ac4a", size = 1063811, upload-time = "2026-08-07T11:28:27.864Z" },
{ url = "https://files.pythonhosted.org/packages/18/1d/84a327c0202a41aa5fdba3ade33904d6d8f3b9e6806fa83568d835395850/ast_serialize-0.8.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:bd84d60bca7079e741be4ac5dbe237751a59d7f6f9f0126b11880d63822cbe16", size = 1105518, upload-time = "2026-08-07T11:28:29.691Z" },
{ url = "https://files.pythonhosted.org/packages/8c/92/74556dec52fde85a2ad84ed159991b916241043788609c15d8b77e14570b/ast_serialize-0.8.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:057769b5921336eb2d9124f2a731b42ed05ffdac559b840dbdf6f3937cf153dc", size = 1076319, upload-time = "2026-08-07T11:28:31.282Z" },
{ url = "https://files.pythonhosted.org/packages/d1/5d/c650b1f2cc1e75193358da95a080261422e8cd10b66d7370b1688c9915c5/ast_serialize-0.8.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:a02cbed7d8bfdcdee88edaac12bd50d53d9953aaa2e1852ef078625be5f1c0b5", size = 852914, upload-time = "2026-08-07T11:28:32.929Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e3/6142e920fec6ef7bccabd8c24ed8ed99f8bdc6cb8b065e1df7c6a3b2d667/ast_serialize-0.8.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e1bd223df0f6c96b396975fa604cb33bce53d9b4a0185490be4c4a289f7c9c87", size = 1184007, upload-time = "2026-08-07T11:28:34.654Z" },
{ url = "https://files.pythonhosted.org/packages/a6/e9/6e8be8df02b35d85e2b8809f7f1cfa290bdf5882b55127a539d049482db0/ast_serialize-0.8.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ddd3b61f45c132da66c5476b281891e08c1fd87fbdabe8a6973e1622efc85f06", size = 1177588, upload-time = "2026-08-07T11:28:36.318Z" },
{ url = "https://files.pythonhosted.org/packages/8c/80/7e0fd2e2e2aba257820db4a8657c4c356844d36b914b20a4af294bcfb902/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f9caa63fad8241257ae401b5ff0a64026c6adb36b8e86cbe8782d9ea505daf6", size = 1234575, upload-time = "2026-08-07T11:28:37.772Z" },
{ url = "https://files.pythonhosted.org/packages/b0/6a/3bae0af06f9b1bae3001c44d64215f5b567877e7aae9ffd45db11c3a7647/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3926fa117b5e65019853a2969966d11c7175af377a3425991f3fe73784412405", size = 1236015, upload-time = "2026-08-07T11:28:39.14Z" },
{ url = "https://files.pythonhosted.org/packages/6f/c4/ce2d41a1bc22508e82618901f7e10f2a5e2f9556553fea90624daf9875e2/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485f1113af805e9e170b95ef993ca3fbd4f89c04bab25c58b4fc632d854801ab", size = 1432808, upload-time = "2026-08-07T11:28:40.664Z" },
{ url = "https://files.pythonhosted.org/packages/1a/90/f5058f209756dd70e958b7538aaa82d25d24944baf9ec8ae6f27b06fcacc/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccebbed24f1281062d5852353c72c47502955926cfcb8345ffb3a44d87ff3d3", size = 1256251, upload-time = "2026-08-07T11:28:42.223Z" },
{ url = "https://files.pythonhosted.org/packages/bf/32/7f77ea87fa0836daab706ed5cb7f903bb25fa26a77439011aee626af11d8/ast_serialize-0.8.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:252f883290d1cdb728eb7fe1d9a7221b88af5a329aae0bc91ddee4dafb820331", size = 1258574, upload-time = "2026-08-07T11:28:43.751Z" },
{ url = "https://files.pythonhosted.org/packages/eb/5a/75b82ad2725b5e8e8c742732f9e76c6738a292d0709e1f60d10a973730b4/ast_serialize-0.8.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:96abc072ad29db8d02194afd47d68987322622787daceae82398d7b69f3ba2e6", size = 1254075, upload-time = "2026-08-07T11:28:45.28Z" },
{ url = "https://files.pythonhosted.org/packages/4e/54/8c20ed4eea805516a3fd23dd4a721ce28c64f50f0e4b359969f60a8c97a6/ast_serialize-0.8.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9118ad3e369727060b2696fc4078f250ecffca4248ba87f537f55cea9f9dce06", size = 1301018, upload-time = "2026-08-07T11:28:46.851Z" },
{ url = "https://files.pythonhosted.org/packages/cb/5b/9f14430f12fe830b656fb38f8e2e05ee13b02a88967660bef46af0ab22a8/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f359df4bd921918af8bebd142a376c77511d7151cc8ba852760b587b5a4a54f3", size = 1409951, upload-time = "2026-08-07T11:28:48.312Z" },
{ url = "https://files.pythonhosted.org/packages/2d/3d/084882eca93c842bd4262591a071ec7f825340644035e51501208cc5a8d4/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:e94f9121d13fa36cbf21314783c77d05ae3a0868decd18cf5233fdcc6de49ac8", size = 1509544, upload-time = "2026-08-07T11:28:49.847Z" },
{ url = "https://files.pythonhosted.org/packages/ce/73/ea84852096c2036c61cc0b2f97b90242207419f534dc671060ee1c8e05cb/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:54f95b486018d262bcb387a9afd96f0da74508b442762b80c769454a6fbb3ee3", size = 1505671, upload-time = "2026-08-07T11:28:51.239Z" },
{ url = "https://files.pythonhosted.org/packages/cb/88/287b9a5300c1f2f651d259f670931b63110adc265b7613c885b44c5bc53d/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c38b915511e32bc718c49dbce98ff9af36bac0ad6a604f58000cd5e3aecdba7", size = 1563685, upload-time = "2026-08-07T11:28:53.112Z" },
{ url = "https://files.pythonhosted.org/packages/ee/f3/1bc3a79afcf0c2a8d2c37182d0d659d1545a9d7f7f6dc9cf3e63d6c17135/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:9a2ef9cf12f2de4f1028c42c1dd7d775255e0fb3e5bb48896c97e35ef52366fe", size = 1427977, upload-time = "2026-08-07T11:28:54.418Z" },
{ url = "https://files.pythonhosted.org/packages/5c/cd/440c798957e14e31776bfeb024d8fafe0bb1d5b89c51c2f067e69938f7b0/ast_serialize-0.8.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f18048fe9f6dd266bd577cdec48bdcecb74faaa01fe941324435483b013ed2a", size = 1454335, upload-time = "2026-08-07T11:28:55.968Z" },
{ url = "https://files.pythonhosted.org/packages/4f/4a/587eb36dcc240a54c8660f599464516b469ecad96f0dbdb6bccbedb50745/ast_serialize-0.8.0-cp39-abi3-win32.whl", hash = "sha256:31883542dd6c94d178f5db3d32fbd69c5eb88b3a7c018e7ac8cc0c45195ddbed", size = 1068858, upload-time = "2026-08-07T11:28:57.541Z" },
{ url = "https://files.pythonhosted.org/packages/5f/a4/3e887bbd92164e183cb6e412c6a3e9198ddd446d7fe405958293ef5ef49c/ast_serialize-0.8.0-cp39-abi3-win_amd64.whl", hash = "sha256:861794565b06337005c1447ef23103a3d5a627d08bdc827870d00d0b28ef5f51", size = 1111839, upload-time = "2026-08-07T11:28:59Z" },
{ url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" },
]
[[package]] [[package]]
name = "authlib" name = "authlib"
version = "1.6.8" version = "1.6.8"
@@ -147,7 +211,7 @@ wheels = [
[[package]] [[package]]
name = "brewman" name = "brewman"
version = "14.0.0" version = "14.2.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "alembic" }, { name = "alembic" },
@@ -173,6 +237,7 @@ dependencies = [
dev = [ dev = [
{ name = "bandit" }, { name = "bandit" },
{ name = "mypy" }, { name = "mypy" },
{ name = "pandas-stubs" },
{ name = "pre-commit" }, { name = "pre-commit" },
{ name = "ruff" }, { name = "ruff" },
{ name = "safety" }, { name = "safety" },
@@ -181,33 +246,34 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "alembic", specifier = ">=1.18.4" }, { name = "alembic", specifier = ">=1.19.1" },
{ name = "arq", specifier = ">=0.28.0" }, { name = "arq", specifier = ">=0.28.0" },
{ name = "cryptography", specifier = ">=48.0.0" }, { name = "cryptography", specifier = ">=50.0.0" },
{ name = "fastapi", extras = ["all"], specifier = ">=0.136.1" }, { name = "fastapi", extras = ["all"], specifier = ">=0.141.1" },
{ name = "gunicorn", specifier = ">=26.0.0" }, { name = "gunicorn", specifier = ">=26.2.0" },
{ name = "itsdangerous", specifier = ">=2.2.0" }, { name = "itsdangerous", specifier = ">=2.2.0" },
{ name = "openpyxl", specifier = ">=3.1.5" }, { name = "openpyxl", specifier = ">=3.1.5" },
{ name = "pandas", specifier = ">=3.0.2" }, { name = "pandas", specifier = ">=3.0.5" },
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" }, { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
{ name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.3.4" }, { name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.3.4" },
{ name = "pydantic", specifier = ">=2.13.3" }, { name = "pydantic", specifier = ">=2.13.4" },
{ name = "pydantic-settings", specifier = ">=2.14.0" }, { name = "pydantic-settings", specifier = ">=2.15.0" },
{ name = "pyjwt", specifier = ">=2.12.1" }, { name = "pyjwt", specifier = ">=2.13.0" },
{ name = "python-dotenv", specifier = ">=1.2.2" }, { name = "python-dotenv", specifier = ">=1.2.3" },
{ name = "python-multipart", specifier = ">=0.0.27" }, { name = "python-multipart", specifier = ">=0.0.32" },
{ name = "sqlalchemy", specifier = ">=2.0.49" }, { name = "sqlalchemy", specifier = ">=2.0.52" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.46.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.52.4" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "bandit", specifier = ">=1.9.4" }, { name = "bandit", specifier = ">=1.9.4" },
{ name = "mypy", specifier = ">=1.20.2" }, { name = "mypy", specifier = ">=2.3.1" },
{ name = "pre-commit", specifier = ">=4.6.0" }, { name = "pandas-stubs", specifier = ">=3.0.5.260730" },
{ name = "ruff", specifier = ">=0.15.12" }, { name = "pre-commit", specifier = ">=4.6.2" },
{ name = "safety", specifier = ">=3.7.0" }, { name = "ruff", specifier = ">=0.16.4" },
{ name = "types-openpyxl", specifier = ">=3.1.5.20260408" }, { name = "safety", specifier = ">=3.8.1" },
{ name = "types-openpyxl", specifier = ">=3.1.5.20260807" },
] ]
[[package]] [[package]]
@@ -261,31 +327,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
] ]
[[package]]
name = "charset-normalizer"
version = "3.4.4"
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" }
wheels = [
{ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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" },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.3.1" version = "8.3.1"
@@ -309,55 +350,52 @@ wheels = [
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "48.0.0" version = "50.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/9f/a9/db8f313fdcd85d767d4973515e1db101f9c71f95fced83233de224673757/cryptography-48.0.0.tar.gz", hash = "sha256:5c3932f4436d1cccb036cb0eaef46e6e2db91035166f1ad6505c3c9d5a635920", size = 832984, upload-time = "2026-05-04T22:59:38.133Z" } sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/3d/01f6dd9190170a5a241e0e98c2d04be3664a9e6f5b9b872cde63aff1c3dd/cryptography-48.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:0c558d2cdffd8f4bbb30fc7134c74d2ca9a476f830bb053074498fbc86f41ed6", size = 8001587, upload-time = "2026-05-04T22:57:36.803Z" }, { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6e/e90527eef33f309beb811cf7c982c3aeffcce8e3edb178baa4ca3ae4a6fa/cryptography-48.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f5333311663ea94f75dd408665686aaf426563556bb5283554a3539177e03b8c", size = 4690433, upload-time = "2026-05-04T22:57:40.373Z" }, { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
{ url = "https://files.pythonhosted.org/packages/90/04/673510ed51ddff56575f306cf1617d80411ee76831ccd3097599140efdfe/cryptography-48.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7995ef305d7165c3f11ae07f2517e5a4f1d5c18da1376a0a9ed496336b69e5f3", size = 4710620, upload-time = "2026-05-04T22:57:42.935Z" }, { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
{ url = "https://files.pythonhosted.org/packages/14/d5/e9c4ef932c8d800490c34d8bd589d64a31d5890e27ec9e9ad532be893294/cryptography-48.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:40ba1f85eaa6959837b1d51c9767e230e14612eea4ef110ee8854ada22da1bf5", size = 4696283, upload-time = "2026-05-04T22:57:45.294Z" }, { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
{ url = "https://files.pythonhosted.org/packages/0c/29/174b9dfb60b12d59ecfc6cfa04bc88c21b42a54f01b8aae09bb6e51e4c7f/cryptography-48.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:369a6348999f94bbd53435c894377b20ab95f25a9065c283570e70150d8abc3c", size = 5296573, upload-time = "2026-05-04T22:57:47.933Z" }, { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
{ url = "https://files.pythonhosted.org/packages/95/38/0d29a6fd7d0d1373f0c0c88a04ba20e359b257753ac497564cd660fc1d55/cryptography-48.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a0e692c683f4df67815a2d258b324e66f4738bd7a96a218c826dce4f4bd05d8f", size = 4743677, upload-time = "2026-05-04T22:57:50.067Z" }, { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
{ url = "https://files.pythonhosted.org/packages/30/be/eef653013d5c63b6a490529e0316f9ac14a37602965d4903efed1399f32b/cryptography-48.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:18349bbc56f4743c8b12dc32e2bccb2cf83ee8b69a3bba74ef8ae857e26b3d25", size = 4330808, upload-time = "2026-05-04T22:57:52.301Z" }, { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
{ url = "https://files.pythonhosted.org/packages/84/9e/500463e87abb7a0a0f9f256ec21123ecde0a7b5541a15e840ea54551fd81/cryptography-48.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e8eac43dfca5c4cccc6dad9a80504436fca53bb9bc3100a2386d730fbe6b602", size = 4695941, upload-time = "2026-05-04T22:57:54.603Z" }, { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
{ url = "https://files.pythonhosted.org/packages/e3/dc/7303087450c2ec9e7fbb750e17c2abfbc658f23cbd0e54009509b7cc4091/cryptography-48.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9ccdac7d40688ecb5a3b4a604b8a88c8002e3442d6c60aead1db2a89a041560c", size = 5252579, upload-time = "2026-05-04T22:57:57.207Z" }, { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
{ url = "https://files.pythonhosted.org/packages/d0/c0/7101d3b7215edcdc90c45da544961fd8ed2d6448f77577460fa75a8443f7/cryptography-48.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:bd72e68b06bb1e96913f97dd4901119bc17f39d4586a5adf2d3e47bc2b9d58b5", size = 4743326, upload-time = "2026-05-04T22:57:59.535Z" }, { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
{ url = "https://files.pythonhosted.org/packages/ac/d8/5b833bad13016f562ab9d063d68199a4bd121d18458e439515601d3357ec/cryptography-48.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59baa2cb386c4f0b9905bd6eb4c2a79a69a128408fd31d32ca4d7102d4156321", size = 4826672, upload-time = "2026-05-04T22:58:01.996Z" }, { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
{ url = "https://files.pythonhosted.org/packages/98/e1/7074eb8bf3c135558c73fc2bcf0f5633f912e6fb87e868a55c454080ef09/cryptography-48.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9249e3cd978541d665967ac2cb2787fd6a62bddf1e75b3e347a594d7dacf4f74", size = 4972574, upload-time = "2026-05-04T22:58:03.968Z" }, { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
{ url = "https://files.pythonhosted.org/packages/04/70/e5a1b41d325f797f39427aa44ef8baf0be500065ab6d8e10369d850d4a4f/cryptography-48.0.0-cp311-abi3-win32.whl", hash = "sha256:9c459db21422be75e2809370b829a87eb37f74cd785fc4aa9ea1e5f43b47cda4", size = 3294868, upload-time = "2026-05-04T22:58:06.467Z" }, { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
{ url = "https://files.pythonhosted.org/packages/f4/ac/8ac51b4a5fc5932eb7ee5c517ba7dc8cd834f0048962b6b352f00f41ebf9/cryptography-48.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:5b012212e08b8dd5edc78ef54da83dd9892fd9105323b3993eff6bea65dc21d7", size = 3817107, upload-time = "2026-05-04T22:58:08.845Z" }, { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
{ url = "https://files.pythonhosted.org/packages/6b/84/70e3feea9feea87fd7cbe77efb2712ae1e3e6edf10749dc6e95f4e60e455/cryptography-48.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:3cb07a3ed6431663cd321ea8a000a1314c74211f823e4177fefa2255e057d1ec", size = 7986556, upload-time = "2026-05-04T22:58:11.172Z" }, { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
{ url = "https://files.pythonhosted.org/packages/89/6e/18e07a618bb5442ba10cf4df16e99c071365528aa570dfcb8c02e25a303b/cryptography-48.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c7378637d7d88016fa6791c159f698b3d3eed28ebf844ac36b9dc04a14dae18", size = 4684776, upload-time = "2026-05-04T22:58:13.712Z" }, { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
{ url = "https://files.pythonhosted.org/packages/be/6a/4ea3b4c6c6759794d5ee2103c304a5076dc4b19ae1f9fe47dba439e159e9/cryptography-48.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc90c0b39b2e3c65ef52c804b72e3c58f8a04ab2a1871272798e5f9572c17d20", size = 4698121, upload-time = "2026-05-04T22:58:16.448Z" }, { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
{ url = "https://files.pythonhosted.org/packages/2f/59/6ff6ad6cae03bb887da2a5860b2c9805f8dac969ef01ce563336c49bd1d1/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:76341972e1eff8b4bea859f09c0d3e64b96ce931b084f9b9b7db8ef364c30eff", size = 4690042, upload-time = "2026-05-04T22:58:18.544Z" }, { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
{ url = "https://files.pythonhosted.org/packages/ca/b4/fc334ed8cfd705aca282fe4d8f5ae64a8e0f74932e9feecb344610cf6e4d/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:55b7718303bf06a5753dcdccf2f3945cf18ad7bffde41b61226e4db31ab89a9c", size = 5282526, upload-time = "2026-05-04T22:58:20.75Z" }, { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
{ url = "https://files.pythonhosted.org/packages/11/08/9f8c5386cc4cd90d8255c7cdd0f5baf459a08502a09de30dc51f553d38dc/cryptography-48.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:a64697c641c7b1b2178e573cbc31c7c6684cd56883a478d75143dbb7118036db", size = 4733116, upload-time = "2026-05-04T22:58:23.627Z" }, { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
{ url = "https://files.pythonhosted.org/packages/b8/77/99307d7574045699f8805aa500fa0fb83422d115b5400a064ddd306d7750/cryptography-48.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:561215ea3879cb1cbbf272867e2efda62476f240fb58c64de6b393ae19246741", size = 4316030, upload-time = "2026-05-04T22:58:25.581Z" }, { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
{ url = "https://files.pythonhosted.org/packages/fd/36/a608b98337af3cb2aff4818e406649d30572b7031918b04c87d979495348/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ad64688338ed4bc1a6618076ba75fd7194a5f1797ac60b47afe926285adb3166", size = 4689640, upload-time = "2026-05-04T22:58:27.747Z" }, { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
{ url = "https://files.pythonhosted.org/packages/dd/a6/825010a291b4438aecc1f568bc428189fc1175515223632477c07dc0a6df/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:906cbf0670286c6e0044156bc7d4af9cbb0ef6db9f73e52c3ec56ba6bdde5336", size = 5237657, upload-time = "2026-05-04T22:58:29.848Z" }, { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
{ url = "https://files.pythonhosted.org/packages/b9/09/4e76a09b4caa29aad535ddc806f5d4c5d01885bd978bd984fbc6ca032cae/cryptography-48.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:ea8990436d914540a40ab24b6a77c0969695ed52f4a4874c5137ccf7045a7057", size = 4732362, upload-time = "2026-05-04T22:58:32.009Z" }, { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
{ url = "https://files.pythonhosted.org/packages/18/78/444fa04a77d0cb95f417dda20d450e13c56ba8e5220fc892a1658f44f882/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c18684a7f0cc9a3cb60328f496b8e3372def7c5d2df39ac267878b05565aaaae", size = 4819580, upload-time = "2026-05-04T22:58:34.254Z" }, { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
{ url = "https://files.pythonhosted.org/packages/38/85/ea67067c70a1fd4be2c63d35eeed82658023021affccc7b17705f8527dd2/cryptography-48.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9be5aafa5736574f8f15f262adc81b2a9869e2cfe9014d52a44633905b40d52c", size = 4963283, upload-time = "2026-05-04T22:58:36.376Z" }, { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
{ url = "https://files.pythonhosted.org/packages/75/54/cc6d0f3deac3e81c7f847e8a189a12b6cdd65059b43dad25d4316abd849a/cryptography-48.0.0-cp314-cp314t-win32.whl", hash = "sha256:c17dfe85494deaeddc5ce251aebd1d60bbe6afc8b62071bb0b469431a000124f", size = 3270954, upload-time = "2026-05-04T22:58:38.791Z" }, { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
{ url = "https://files.pythonhosted.org/packages/49/67/cc947e288c0758a4e5473d1dcb743037ab7785541265a969240b8885441a/cryptography-48.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27241b1dc9962e056062a8eef1991d02c3a24569c95975bd2322a8a52c6e5e12", size = 3797313, upload-time = "2026-05-04T22:58:40.746Z" }, { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
{ url = "https://files.pythonhosted.org/packages/f2/63/61d4a4e1c6b6bab6ce1e213cd36a24c415d90e76d78c5eb8577c5541d2e8/cryptography-48.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:58d00498e8933e4a194f3076aee1b4a97dfec1a6da444535755822fe5d8b0b86", size = 7983482, upload-time = "2026-05-04T22:58:43.769Z" }, { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ac/f5b5995b87770c693e2596559ffafe195b4033a57f14a82268a2842953f3/cryptography-48.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:614d0949f4790582d2cc25553abd09dd723025f0c0e7c67376a1d77196743d6e", size = 4683266, upload-time = "2026-05-04T22:58:46.064Z" }, { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
{ url = "https://files.pythonhosted.org/packages/ec/c6/8b14f67e18338fbc4adb76f66c001f5c3610b3e2d1837f268f47a347dbbb/cryptography-48.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ce4bfae76319a532a2dc68f82cc32f5676ee792a983187dac07183690e5c66f", size = 4696228, upload-time = "2026-05-04T22:58:48.22Z" }, { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
{ url = "https://files.pythonhosted.org/packages/ea/73/f808fbae9514bd91b47875b003f13e284c8c6bdfd904b7944e803937eec1/cryptography-48.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:2eb992bbd4661238c5a397594c83f5b4dc2bc5b848c365c8f991b6780efcc5c7", size = 4689097, upload-time = "2026-05-04T22:58:50.9Z" }, { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
{ url = "https://files.pythonhosted.org/packages/93/01/d86632d7d28db8ae83221995752eeb6639ffb374c2d22955648cf8d52797/cryptography-48.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:22a5cb272895dce158b2cacdfdc3debd299019659f42947dbdac6f32d68fe832", size = 5283582, upload-time = "2026-05-04T22:58:53.017Z" }, { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
{ url = "https://files.pythonhosted.org/packages/02/e1/50edc7a50334807cc4791fc4a0ce7468b4a1416d9138eab358bfc9a3d70b/cryptography-48.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2b4d59804e8408e2fea7d1fbaf218e5ec984325221db76e6a241a9abd6cdd95c", size = 4730479, upload-time = "2026-05-04T22:58:55.611Z" }, { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
{ url = "https://files.pythonhosted.org/packages/6f/af/99a582b1b1641ff5911ac559beb45097cf79efd4ead4657f578ef1af2d47/cryptography-48.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:984a20b0f62a26f48a3396c72e4bc34c66e356d356bf370053066b3b6d54634a", size = 4326481, upload-time = "2026-05-04T22:58:57.607Z" }, { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
{ url = "https://files.pythonhosted.org/packages/90/ee/89aa26a06ef0a7d7611788ffd571a7c50e368cc6a4d5eef8b4884e866edb/cryptography-48.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:5a5ed8fde7a1d09376ca0b40e68cd59c69fe23b1f9768bd5824f54681626032a", size = 4688713, upload-time = "2026-05-04T22:59:00.077Z" }, { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
{ url = "https://files.pythonhosted.org/packages/70/ba/bcb1b0bb7a33d4c7c0c4d4c7874b4a62ae4f56113a5f4baefa362dfb1f0f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:8cd666227ef7af430aa5914a9910e0ddd703e75f039cef0825cd0da71b6b711a", size = 5238165, upload-time = "2026-05-04T22:59:02.317Z" }, { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
{ url = "https://files.pythonhosted.org/packages/c9/70/ca4003b1ce5ca3dc3186ada51908c8a9b9ff7d5cab83cc0d43ee14ec144f/cryptography-48.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9071196d81abc88b3516ac8cdfad32e2b66dd4a5393a8e68a961e9161ddc6239", size = 4729947, upload-time = "2026-05-04T22:59:05.255Z" }, { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
{ url = "https://files.pythonhosted.org/packages/44/a0/4ec7cf774207905aef1a8d11c3750d5a1db805eb380ee4e16df317870128/cryptography-48.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e2d54c8be6152856a36f0882ab231e70f8ec7f14e93cf87db8a2ed056bf160c", size = 4822059, upload-time = "2026-05-04T22:59:07.802Z" }, { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
{ url = "https://files.pythonhosted.org/packages/1e/75/a2e55f99c16fcac7b5d6c1eb19ad8e00799854d6be5ca845f9259eae1681/cryptography-48.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5da777e32ffed6f85a7b2b3f7c5cbc88c146bfcd0a1d7baf5fcc6c52ee35dd4", size = 4960575, upload-time = "2026-05-04T22:59:09.851Z" },
{ url = "https://files.pythonhosted.org/packages/b8/23/6e6f32143ab5d8b36ca848a502c4bcd477ae75b9e1677e3530d669062578/cryptography-48.0.0-cp39-abi3-win32.whl", hash = "sha256:77a2ccbbe917f6710e05ba9adaa25fb5075620bf3ea6fb751997875aff4ae4bd", size = 3279117, upload-time = "2026-05-04T22:59:12.019Z" },
{ url = "https://files.pythonhosted.org/packages/9d/9a/0fea98a70cf1749d41d738836f6349d97945f7c89433a259a6c2642eefeb/cryptography-48.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:16cd65b9330583e4619939b3a3843eec1e6e789744bb01e7c7e2e62e33c239c8", size = 3792100, upload-time = "2026-05-04T22:59:14.884Z" },
] ]
[[package]] [[package]]
@@ -414,7 +452,7 @@ wheels = [
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.136.1" version = "0.141.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "annotated-doc" }, { name = "annotated-doc" },
@@ -423,9 +461,9 @@ dependencies = [
{ name = "typing-extensions" }, { name = "typing-extensions" },
{ name = "typing-inspection" }, { name = "typing-inspection" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5d/45/c130091c2dfa061bbfe3150f2a5091ef1adf149f2a8d2ae769ecaf6e99a2/fastapi-0.136.1.tar.gz", hash = "sha256:7af665ad7acfa0a3baf8983d393b6b471b9da10ede59c60045f49fbc89a0fa7f", size = 397448, upload-time = "2026-04-23T16:49:44.046Z" } sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -444,16 +482,16 @@ all = [
[[package]] [[package]]
name = "fastapi-cli" name = "fastapi-cli"
version = "0.0.23" version = "0.0.32"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "rich-toolkit" }, { name = "rich-toolkit" },
{ name = "typer" }, { name = "typer" },
{ name = "uvicorn", extra = ["standard"] }, { name = "uvicorn", extra = ["standard"] },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/71/9f/cbd463e57de4e977b8ea0403f95347f9150441568b1d3fe3e4949ef80ef3/fastapi_cli-0.0.23.tar.gz", hash = "sha256:210ac280ea41e73aac5a57688781256beb23c2cba3a41266896fa43e6445c8e7", size = 19763, upload-time = "2026-02-16T19:45:53.358Z" } sdist = { url = "https://files.pythonhosted.org/packages/33/eb/3b534c6f8e157f9ddbf2a153512307c886cad0b258739c200dd8ff8c4452/fastapi_cli-0.0.32.tar.gz", hash = "sha256:38024d2345275e1b37ce8848727a580d84901b570e96b3256d9d36a9a5039424", size = 26636, upload-time = "2026-07-16T12:16:58.678Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/68/89/19dcfd5cd289b306abdcabac68b88a4f54b7710a2c33adc16a337ecdcdfa/fastapi_cli-0.0.23-py3-none-any.whl", hash = "sha256:7e9634fc212da0b6cfc75bd3ac366cc9dfdb43b5e9ec12e58bfd1acdd2697f25", size = 12305, upload-time = "2026-02-16T19:45:52.554Z" }, { url = "https://files.pythonhosted.org/packages/d5/53/56ae5ae17bb0a5d89d1d31e5320eb1865553ebbfbde91cdc4c221245f2a8/fastapi_cli-0.0.32-py3-none-any.whl", hash = "sha256:8dcc286fa32f01bbd3f65dd09cfd5a2540ed5f2230b77db7fd30978d6165f3c4", size = 14670, upload-time = "2026-07-16T12:16:57.297Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -553,14 +591,11 @@ wheels = [
[[package]] [[package]]
name = "gunicorn" name = "gunicorn"
version = "26.0.0" version = "26.2.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ sdist = { url = "https://files.pythonhosted.org/packages/d9/8a/e4ef6ee11701b6cd64702848415ffb69eeff85cb388a3c6c7fe86f22f3f8/gunicorn-26.2.0.tar.gz", hash = "sha256:62b864895d9ebff0b2f9867ba04fe811c93121596540830c9c916d0769668447", size = 787921, upload-time = "2026-08-24T15:05:59.3Z" }
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6d/b7/a4a3f632f823e432ce6bc65f62961b7980c898c77f075a2f7118cb3846fe/gunicorn-26.0.0.tar.gz", hash = "sha256:ca9346f85e3a4aeeb64d491045c16b9a35647abd37ea15efe53080eb8b090baf", size = 727286, upload-time = "2026-05-05T06:38:25.529Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/40/9c2384fc2be4ad25dd4a49decd5ad9ea5a3639814c11bd40ab77cb9f0a14/gunicorn-26.0.0-py3-none-any.whl", hash = "sha256:40233d26a5f0d1872916188c276e21641155111c2853f0c2cd55260aec0d24fc", size = 212009, upload-time = "2026-05-05T06:38:23.007Z" }, { url = "https://files.pythonhosted.org/packages/fe/85/7522a52e5e2f42faf1a129113ab63e548c42e103e9af395b7bfe65e403e2/gunicorn-26.2.0-py3-none-any.whl", hash = "sha256:bd249d0b3f7972f7432f0a6b6ff3b3ee2d129f70cd1ff6c09a9dd9e29a2b88e3", size = 228389, upload-time = "2026-08-24T15:05:57.67Z" },
] ]
[[package]] [[package]]
@@ -621,17 +656,24 @@ wheels = [
[[package]] [[package]]
name = "httptools" name = "httptools"
version = "0.7.1" version = "0.8.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" },
{ url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" },
{ url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" },
{ url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" },
{ url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" },
{ url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" },
{ url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" },
{ url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" },
{ url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" },
{ url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" },
{ url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" },
{ url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" },
{ url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" },
{ url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" },
] ]
[[package]] [[package]]
@@ -699,36 +741,72 @@ wheels = [
[[package]] [[package]]
name = "librt" name = "librt"
version = "0.8.1" version = "0.15.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" },
{ url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" },
{ url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" },
{ url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" },
{ url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" },
{ url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" },
{ url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" },
{ url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" },
{ url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" },
{ url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" },
{ url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" },
{ url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" },
{ url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" },
{ url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" },
{ url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" },
{ url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" },
{ url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" },
{ url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" },
{ url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" },
{ url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" },
{ url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" },
{ url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" },
{ url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" },
{ url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" },
{ url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" },
{ url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" },
{ url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" },
{ url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" },
{ url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" },
{ url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" },
{ url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" },
{ url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" },
{ url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" },
{ url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" },
{ url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" },
{ url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" },
{ url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" },
{ url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" },
{ url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" },
{ url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" },
{ url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" },
{ url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" },
{ url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" },
{ url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" },
{ url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" },
{ url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" },
{ url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" },
{ url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" },
{ url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" },
{ url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" },
{ url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" },
{ url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" },
{ url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" },
{ url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" },
{ url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" },
{ url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" },
{ url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" },
{ url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" },
{ url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" },
{ url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" },
{ url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" },
{ url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" },
] ]
[[package]] [[package]]
@@ -805,31 +883,44 @@ wheels = [
[[package]] [[package]]
name = "mypy" name = "mypy"
version = "1.20.2" version = "2.3.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "ast-serialize" },
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" }, { name = "mypy-extensions" },
{ name = "pathspec" }, { name = "pathspec" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" }, { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" },
{ url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" }, { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" },
{ url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" }, { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" },
{ url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" }, { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" },
{ url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" }, { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" },
{ url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" }, { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" },
{ url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" }, { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" },
{ url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" }, { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" },
{ url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" }, { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" },
{ url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" }, { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" },
{ url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" }, { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" },
{ url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" }, { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" },
{ url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" }, { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" },
{ url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" }, { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" },
{ url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" },
{ url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" },
{ url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" },
{ url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" },
{ url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" },
{ url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" },
{ url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" },
{ url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" },
{ url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" },
{ url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" },
{ url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" },
{ url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" },
{ url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" },
] ]
[[package]] [[package]]
@@ -917,31 +1008,43 @@ wheels = [
[[package]] [[package]]
name = "pandas" name = "pandas"
version = "3.0.2" version = "3.0.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "numpy" }, { name = "numpy" },
{ name = "python-dateutil" }, { name = "python-dateutil" },
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" },
{ url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" },
{ url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" },
{ url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" },
{ url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" },
{ url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" },
{ url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" },
{ url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" },
{ url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" },
{ url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" },
{ url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" },
{ url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" },
{ url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" },
{ url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" },
{ url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" },
{ url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" },
]
[[package]]
name = "pandas-stubs"
version = "3.0.5.260730"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/d2/dea4a3a56b7b5f69c5fbca9f14625fcf28e1a39a657e9833d4a10bcac593/pandas_stubs-3.0.5.260730.tar.gz", hash = "sha256:f70a232c57d93a5a2c81f8a53953e10891a5374bc92652277deb325e2e4d0ff3", size = 114631, upload-time = "2026-07-30T14:31:42.271Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/c2/959caec5c46f484b5f8bb6def4b0cf7a45ba6acda26f12b75142d98cc5ae/pandas_stubs-3.0.5.260730-py3-none-any.whl", hash = "sha256:60e90e3e1eda6937e337e243cbe6217e151c11137cd7eddf832af537c7310bfd", size = 174807, upload-time = "2026-07-30T14:31:41.17Z" },
] ]
[[package]] [[package]]
@@ -978,7 +1081,7 @@ wheels = [
[[package]] [[package]]
name = "pre-commit" name = "pre-commit"
version = "4.6.0" version = "4.6.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cfgv" }, { name = "cfgv" },
@@ -987,9 +1090,9 @@ dependencies = [
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "virtualenv" }, { name = "virtualenv" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } sdist = { url = "https://files.pythonhosted.org/packages/74/89/1f3e8e1fc3e97de0fa963495832f581f025f29471602a309e48808244292/pre_commit-4.6.2.tar.gz", hash = "sha256:8f5d7bfb021ecdbcd9d49d89847082dd24172ccde534390081a679ad046e2441", size = 198670, upload-time = "2026-08-10T22:07:18.421Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, { url = "https://files.pythonhosted.org/packages/45/e2/bbb7129c9e7999a6b8ee9cca3b66486c25c423ab5a75f34071798b74ce94/pre_commit-4.6.2-py2.py3-none-any.whl", hash = "sha256:e2dde9a75d3bce11bd3831c26d134df00a2803c1d818be6a0383c3dcda25dc4e", size = 226202, upload-time = "2026-08-10T22:07:16.942Z" },
] ]
[[package]] [[package]]
@@ -1053,7 +1156,7 @@ wheels = [
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.13.3" version = "2.13.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "annotated-types" }, { name = "annotated-types" },
@@ -1061,9 +1164,9 @@ dependencies = [
{ name = "typing-extensions" }, { name = "typing-extensions" },
{ name = "typing-inspection" }, { name = "typing-inspection" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/d9/e4/40d09941a2cebcb20609b86a559817d5b9291c49dd6f8c87e5feffbe703a/pydantic-2.13.3.tar.gz", hash = "sha256:af09e9d1d09f4e7fe37145c1f577e1d61ceb9a41924bf0094a36506285d0a84d", size = 844068, upload-time = "2026-04-20T14:46:43.632Z" } sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/0a/fd7d723f8f8153418fb40cf9c940e82004fce7e987026b08a68a36dd3fe7/pydantic-2.13.3-py3-none-any.whl", hash = "sha256:6db14ac8dfc9a1e57f87ea2c0de670c251240f43cb0c30a5130e9720dc612927", size = 471981, upload-time = "2026-04-20T14:46:41.402Z" }, { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -1073,43 +1176,43 @@ email = [
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
version = "2.46.3" version = "2.46.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/2a/ef/f7abb56c49382a246fd2ce9c799691e3c3e7175ec74b14d99e798bcddb1a/pydantic_core-2.46.3.tar.gz", hash = "sha256:41c178f65b8c29807239d47e6050262eb6bf84eb695e41101e62e38df4a5bc2c", size = 471412, upload-time = "2026-04-20T14:40:56.672Z" } sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/db/a7bcb4940183fda36022cd18ba8dd12f2dff40740ec7b58ce7457befa416/pydantic_core-2.46.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:afa3aa644f74e290cdede48a7b0bee37d1c35e71b05105f6b340d484af536d9b", size = 2097614, upload-time = "2026-04-20T14:44:38.374Z" }, { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
{ url = "https://files.pythonhosted.org/packages/24/35/e4066358a22e3e99519db370494c7528f5a2aa1367370e80e27e20283543/pydantic_core-2.46.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ced3310e51aa425f7f77da8bbbb5212616655bedbe82c70944320bc1dbe5e018", size = 1951896, upload-time = "2026-04-20T14:40:53.996Z" }, { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
{ url = "https://files.pythonhosted.org/packages/87/92/37cf4049d1636996e4b888c05a501f40a43ff218983a551d57f9d5e14f0d/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e29908922ce9da1a30b4da490bd1d3d82c01dcfdf864d2a74aacee674d0bfa34", size = 1979314, upload-time = "2026-04-20T14:41:49.446Z" }, { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
{ url = "https://files.pythonhosted.org/packages/d8/36/9ff4d676dfbdfb2d591cf43f3d90ded01e15b1404fd101180ed2d62a2fd3/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c9ff69140423eea8ed2d5477df3ba037f671f5e897d206d921bc9fdc39613e7", size = 2056133, upload-time = "2026-04-20T14:42:23.574Z" }, { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
{ url = "https://files.pythonhosted.org/packages/bc/f0/405b442a4d7ba855b06eec8b2bf9c617d43b8432d099dfdc7bf999293495/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b675ab0a0d5b1c8fdb81195dc5bcefea3f3c240871cdd7ff9a2de8aa50772eb2", size = 2228726, upload-time = "2026-04-20T14:44:22.816Z" }, { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
{ url = "https://files.pythonhosted.org/packages/e7/f8/65cd92dd5a0bd89ba277a98ecbfaf6fc36bbd3300973c7a4b826d6ab1391/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0087084960f209a9a4af50ecd1fb063d9ad3658c07bb81a7a53f452dacbfb2ba", size = 2301214, upload-time = "2026-04-20T14:44:48.792Z" }, { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
{ url = "https://files.pythonhosted.org/packages/fd/86/ef96a4c6e79e7a2d0410826a68fbc0eccc0fd44aa733be199d5fcac3bb87/pydantic_core-2.46.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed42e6cc8e1b0e2b9b96e2276bad70ae625d10d6d524aed0c93de974ae029f9f", size = 2099927, upload-time = "2026-04-20T14:41:40.196Z" }, { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
{ url = "https://files.pythonhosted.org/packages/6d/53/269caf30e0096e0a8a8f929d1982a27b3879872cca2d917d17c2f9fdf4fe/pydantic_core-2.46.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f1771ce258afb3e4201e67d154edbbae712a76a6081079fe247c2f53c6322c22", size = 2128789, upload-time = "2026-04-20T14:41:15.868Z" }, { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
{ url = "https://files.pythonhosted.org/packages/00/b0/1a6d9b6a587e118482910c244a1c5acf4d192604174132efd12bf0ac486f/pydantic_core-2.46.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a7610b6a5242a6c736d8ad47fd5fff87fcfe8f833b281b1c409c3d6835d9227f", size = 2173815, upload-time = "2026-04-20T14:44:25.152Z" }, { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
{ url = "https://files.pythonhosted.org/packages/87/56/e7e00d4041a7e62b5a40815590114db3b535bf3ca0bf4dca9f16cef25246/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:ff5e7783bcc5476e1db448bf268f11cb257b1c276d3e89f00b5727be86dd0127", size = 2181608, upload-time = "2026-04-20T14:41:28.933Z" }, { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
{ url = "https://files.pythonhosted.org/packages/e8/22/4bd23c3d41f7c185d60808a1de83c76cf5aeabf792f6c636a55c3b1ec7f9/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:9d2e32edcc143bc01e95300671915d9ca052d4f745aa0a49c48d4803f8a85f2c", size = 2326968, upload-time = "2026-04-20T14:42:03.962Z" }, { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
{ url = "https://files.pythonhosted.org/packages/24/ac/66cd45129e3915e5ade3b292cb3bc7fd537f58f8f8dbdaba6170f7cabb74/pydantic_core-2.46.3-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d83d1c6b87fa56b521479cff237e626a292f3b31b6345c15a99121b454c1", size = 2369842, upload-time = "2026-04-20T14:41:35.52Z" }, { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
{ url = "https://files.pythonhosted.org/packages/a2/51/dd4248abb84113615473aa20d5545b7c4cd73c8644003b5259686f93996c/pydantic_core-2.46.3-cp314-cp314-win32.whl", hash = "sha256:07bc6d2a28c3adb4f7c6ae46aa4f2d2929af127f587ed44057af50bf1ce0f505", size = 1959661, upload-time = "2026-04-20T14:41:00.042Z" }, { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
{ url = "https://files.pythonhosted.org/packages/20/eb/59980e5f1ae54a3b86372bd9f0fa373ea2d402e8cdcd3459334430f91e91/pydantic_core-2.46.3-cp314-cp314-win_amd64.whl", hash = "sha256:8940562319bc621da30714617e6a7eaa6b98c84e8c685bcdc02d7ed5e7c7c44e", size = 2071686, upload-time = "2026-04-20T14:43:16.471Z" }, { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
{ url = "https://files.pythonhosted.org/packages/8c/db/1cf77e5247047dfee34bc01fa9bca134854f528c8eb053e144298893d370/pydantic_core-2.46.3-cp314-cp314-win_arm64.whl", hash = "sha256:5dcbbcf4d22210ced8f837c96db941bdb078f419543472aca5d9a0bb7cddc7df", size = 2026907, upload-time = "2026-04-20T14:43:31.732Z" }, { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
{ url = "https://files.pythonhosted.org/packages/57/c0/b3df9f6a543276eadba0a48487b082ca1f201745329d97dbfa287034a230/pydantic_core-2.46.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d0fe3dce1e836e418f912c1ad91c73357d03e556a4d286f441bf34fed2dbeecf", size = 2095047, upload-time = "2026-04-20T14:42:37.982Z" }, { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
{ url = "https://files.pythonhosted.org/packages/66/57/886a938073b97556c168fd99e1a7305bb363cd30a6d2c76086bf0587b32a/pydantic_core-2.46.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9ce92e58abc722dac1bf835a6798a60b294e48eb0e625ec9fd994b932ac5feee", size = 1934329, upload-time = "2026-04-20T14:43:49.655Z" }, { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
{ url = "https://files.pythonhosted.org/packages/0b/7c/b42eaa5c34b13b07ecb51da21761297a9b8eb43044c864a035999998f328/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a03e6467f0f5ab796a486146d1b887b2dc5e5f9b3288898c1b1c3ad974e53e4a", size = 1974847, upload-time = "2026-04-20T14:42:10.737Z" }, { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
{ url = "https://files.pythonhosted.org/packages/e6/9b/92b42db6543e7de4f99ae977101a2967b63122d4b6cf7773812da2d7d5b5/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2798b6ba041b9d70acfb9071a2ea13c8456dd1e6a5555798e41ba7b0790e329c", size = 2041742, upload-time = "2026-04-20T14:40:44.262Z" }, { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
{ url = "https://files.pythonhosted.org/packages/0f/19/46fbe1efabb5aa2834b43b9454e70f9a83ad9c338c1291e48bdc4fecf167/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9be3e221bdc6d69abf294dcf7aff6af19c31a5cdcc8f0aa3b14be29df4bd03b1", size = 2236235, upload-time = "2026-04-20T14:41:27.307Z" }, { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
{ url = "https://files.pythonhosted.org/packages/77/da/b3f95bc009ad60ec53120f5d16c6faa8cabdbe8a20d83849a1f2b8728148/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13936129ce841f2a5ddf6f126fea3c43cd128807b5a59588c37cf10178c2e64", size = 2282633, upload-time = "2026-04-20T14:44:33.271Z" }, { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
{ url = "https://files.pythonhosted.org/packages/cc/6e/401336117722e28f32fb8220df676769d28ebdf08f2f4469646d404c43a3/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28b5f2ef03416facccb1c6ef744c69793175fd27e44ef15669201601cf423acb", size = 2109679, upload-time = "2026-04-20T14:44:41.065Z" }, { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
{ url = "https://files.pythonhosted.org/packages/fc/53/b289f9bc8756a32fe718c46f55afaeaf8d489ee18d1a1e7be1db73f42cc4/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:830d1247d77ad23852314f069e9d7ddafeec5f684baf9d7e7065ed46a049c4e6", size = 2108342, upload-time = "2026-04-20T14:42:50.144Z" }, { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
{ url = "https://files.pythonhosted.org/packages/10/5b/8292fc7c1f9111f1b2b7c1b0dcf1179edcd014fc3ea4517499f50b829d71/pydantic_core-2.46.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0793c90c1a3c74966e7975eaef3ed30ebdff3260a0f815a62a22adc17e4c01c", size = 2157208, upload-time = "2026-04-20T14:42:08.133Z" }, { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9e/f80044e9ec07580f057a89fc131f78dda7a58751ddf52bbe05eaf31db50f/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:d2d0aead851b66f5245ec0c4fb2612ef457f8bbafefdf65a2bf9d6bac6140f47", size = 2167237, upload-time = "2026-04-20T14:42:25.412Z" }, { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
{ url = "https://files.pythonhosted.org/packages/f8/84/6781a1b037f3b96be9227edbd1101f6d3946746056231bf4ac48cdff1a8d/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:2f40e4246676beb31c5ce77c38a55ca4e465c6b38d11ea1bd935420568e0b1ab", size = 2312540, upload-time = "2026-04-20T14:40:40.313Z" }, { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
{ url = "https://files.pythonhosted.org/packages/3e/db/19c0839feeb728e7df03255581f198dfdf1c2aeb1e174a8420b63c5252e5/pydantic_core-2.46.3-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:cf489cf8986c543939aeee17a09c04d6ffb43bfef8ca16fcbcc5cfdcbed24dba", size = 2369556, upload-time = "2026-04-20T14:41:09.427Z" }, { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
{ url = "https://files.pythonhosted.org/packages/e0/15/3228774cb7cd45f5f721ddf1b2242747f4eb834d0c491f0c02d606f09fed/pydantic_core-2.46.3-cp314-cp314t-win32.whl", hash = "sha256:ffe0883b56cfc05798bf994164d2b2ff03efe2d22022a2bb080f3b626176dd56", size = 1949756, upload-time = "2026-04-20T14:41:25.717Z" }, { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
{ url = "https://files.pythonhosted.org/packages/b8/2a/c79cf53fd91e5a87e30d481809f52f9a60dd221e39de66455cf04deaad37/pydantic_core-2.46.3-cp314-cp314t-win_amd64.whl", hash = "sha256:706d9d0ce9cf4593d07270d8e9f53b161f90c57d315aeec4fb4fd7a8b10240d8", size = 2051305, upload-time = "2026-04-20T14:43:18.627Z" }, { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
{ url = "https://files.pythonhosted.org/packages/0b/db/d8182a7f1d9343a032265aae186eb063fe26ca4c40f256b21e8da4498e89/pydantic_core-2.46.3-cp314-cp314t-win_arm64.whl", hash = "sha256:77706aeb41df6a76568434701e0917da10692da28cb69d5fb6919ce5fdb07374", size = 2026310, upload-time = "2026-04-20T14:41:01.778Z" }, { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
] ]
[[package]] [[package]]
@@ -1127,16 +1230,16 @@ wheels = [
[[package]] [[package]]
name = "pydantic-settings" name = "pydantic-settings"
version = "2.14.0" version = "2.15.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "typing-inspection" }, { name = "typing-inspection" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" },
] ]
[[package]] [[package]]
@@ -1150,11 +1253,11 @@ wheels = [
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.12.1" version = "2.13.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" },
] ]
[[package]] [[package]]
@@ -1171,20 +1274,20 @@ wheels = [
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.2.2" version = "1.2.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" },
] ]
[[package]] [[package]]
name = "python-multipart" name = "python-multipart"
version = "0.0.27" version = "0.0.32"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" } sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" }, { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
] ]
[[package]] [[package]]
@@ -1270,21 +1373,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/45/affdf2d851b42adf3d13fc5b3b059372e9bd299371fd84cf5723c45871fa/regex-2026.2.19-cp314-cp314t-win_arm64.whl", hash = "sha256:a09ae430e94c049dc6957f6baa35ee3418a3a77f3c12b6e02883bd80a2b679b0", size = 274932, upload-time = "2026-02-19T19:03:45.488Z" }, { url = "https://files.pythonhosted.org/packages/3a/45/affdf2d851b42adf3d13fc5b3b059372e9bd299371fd84cf5723c45871fa/regex-2026.2.19-cp314-cp314t-win_arm64.whl", hash = "sha256:a09ae430e94c049dc6957f6baa35ee3418a3a77f3c12b6e02883bd80a2b679b0", size = 274932, upload-time = "2026-02-19T19:03:45.488Z" },
] ]
[[package]]
name = "requests"
version = "2.32.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ 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" }
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" },
]
[[package]] [[package]]
name = "rich" name = "rich"
version = "14.3.3" version = "14.3.3"
@@ -1361,35 +1449,36 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.15.12" version = "0.16.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } sdist = { url = "https://files.pythonhosted.org/packages/00/8f/d8074b1f25e003164087a8bfe79a0f1a3945135764dbb6aaab04103dcaf9/ruff-0.16.4.tar.gz", hash = "sha256:13171aa9d9af2240ee3504e639de73122c67e74036de5ba2e1d01422cd17e3dc", size = 4899731, upload-time = "2026-08-20T17:43:59.196Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, { url = "https://files.pythonhosted.org/packages/ff/80/779895ef584e089d22f2c6df0d0e99a65ec2df0805f1fffd439415b8c1f0/ruff-0.16.4-py3-none-linux_armv6l.whl", hash = "sha256:df4075f71ddac40b9934af60c3ec8a53047dd5a5fdc43224e6e4e8e9a27cb6f7", size = 10006909, upload-time = "2026-08-20T17:43:16.888Z" },
{ url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, { url = "https://files.pythonhosted.org/packages/a9/e6/f553199b5e8927a05cb5c422d921fd0656b29ab976e91c44802107c6b0da/ruff-0.16.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0c95538517af68004306b0fb3214ff2f2af67a65092aee77cd9eb86db6656604", size = 10240201, upload-time = "2026-08-20T17:43:19.337Z" },
{ url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, { url = "https://files.pythonhosted.org/packages/1c/70/4a6dc4bb34da4dee35e30f09bbd1bfbdd26f33b62fb9b8df31f08a199cd2/ruff-0.16.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:963f83df8e69e575b64d67dd447ebbc917db41a14bf38d4593a4183e7aaa8255", size = 9835122, upload-time = "2026-08-20T17:43:21.708Z" },
{ url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, { url = "https://files.pythonhosted.org/packages/24/12/c6e22d686372c15bcb7af99831f1a1be96df696491babf4f24e4f942c527/ruff-0.16.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32a5057c7ff3f6e6480a48fccfb3a412a690f48a3d03ac5cf08177d6c2da3ade", size = 9977162, upload-time = "2026-08-20T17:43:24.236Z" },
{ url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, { url = "https://files.pythonhosted.org/packages/46/49/72b10ec912f5ab5854992eaf7aa7cd36729b6937d9dc4e0fb41b3bf428ec/ruff-0.16.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3dce8d9b0c57c265b91885a66a567d8ea1372e8eb4e250fa8e5e3f579e99cff", size = 9829789, upload-time = "2026-08-20T17:43:26.966Z" },
{ url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, { url = "https://files.pythonhosted.org/packages/fa/80/0f30e32e7f6ee26edc39075502db9d368d788a44a79b55f763eb4ab03796/ruff-0.16.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7dc651db49283c69f8e72c834eec4fe5573e4c646856aebece0ce385dceb2a80", size = 10527949, upload-time = "2026-08-20T17:43:29.384Z" },
{ url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, { url = "https://files.pythonhosted.org/packages/52/3d/86e8ad3542169e56cac3859a343afdb9df2ad54d35a59ce1e67baee83421/ruff-0.16.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3817b87dbcabc92f13b05019257c5b89b5b4d51b5fb20f56fb5235ceb723cd07", size = 11333695, upload-time = "2026-08-20T17:43:31.872Z" },
{ url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, { url = "https://files.pythonhosted.org/packages/d0/16/481c29b380c20a0054a8261066665e1b3488e23636c49d0a43e75975b9bb/ruff-0.16.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9fce1499134b2c8c68e5166f95705a5812062bb93aacc5f9873bb1a27084bc7", size = 10727741, upload-time = "2026-08-20T17:43:34.596Z" },
{ url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, { url = "https://files.pythonhosted.org/packages/5e/b6/56bc0b8cf45b54b28b3a5e6381c8945d51b5b18adf659454c32295209a31/ruff-0.16.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2d812e482f5a7e02eee26cd73d2a37ebbdf47d795ea63ba1b89110ae93e9fb3", size = 10286522, upload-time = "2026-08-20T17:43:37.288Z" },
{ url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, { url = "https://files.pythonhosted.org/packages/e8/8b/b345b4fb110f2fbe2bd31eabd271e5e8b3b7e4ee6c0e02f2dc6be78db000/ruff-0.16.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6baaf984aa7976edf93d3b627fe2d1d22ee94bbca05fa6f90fc76d73924e3454", size = 10584182, upload-time = "2026-08-20T17:43:39.984Z" },
{ url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, { url = "https://files.pythonhosted.org/packages/29/e5/827b34041c35f58774a9681a4213994c164fc987800f4dddabcf451da0bf/ruff-0.16.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bdfcf0b28662eb890372d50f92c283bb94e67e7635ed93c7fd533970acff7b2b", size = 10134195, upload-time = "2026-08-20T17:43:42.351Z" },
{ url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, { url = "https://files.pythonhosted.org/packages/0f/10/d0bffcdd6729b87afc82ba0ef377173356a7dc8e972f5179968cf2fdf98c/ruff-0.16.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b66b02cb9b04f537643cadf5768e5f98dc461890d530cb67113d71c8c76e605d", size = 9825821, upload-time = "2026-08-20T17:43:44.532Z" },
{ url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, { url = "https://files.pythonhosted.org/packages/f5/32/0db2a863b796ca62d83e92a07a3ccf00921b14db02059347576a2fda3d4b/ruff-0.16.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:8528bf9a4b291a60bf02ea453511e8ce6215bd2b982ee80405b66b008b6c30a0", size = 10267658, upload-time = "2026-08-20T17:43:46.989Z" },
{ url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, { url = "https://files.pythonhosted.org/packages/b2/a0/fbdeb59e48c6261f523e56c8f12e9c08fbe693786595cc7e3959207a9232/ruff-0.16.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:fbd85d2875fdd67e833213a651f613bbf25303abf6aa822a5121f4531195678d", size = 10697071, upload-time = "2026-08-20T17:43:49.891Z" },
{ url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, { url = "https://files.pythonhosted.org/packages/aa/28/0c6dd865859c6d17bc8ccc34cb72b0e02d6c7eb25e8a1e22b5bea681e2c0/ruff-0.16.4-py3-none-win32.whl", hash = "sha256:312769988007aaeb8e189b443ccdd03c0e6374489e053467be6d96518ebff76e", size = 10021687, upload-time = "2026-08-20T17:43:52.281Z" },
{ url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, { url = "https://files.pythonhosted.org/packages/a3/03/e724450f621698117f9aa6dd241c94d0274ae96781378dc86745ae29f0e7/ruff-0.16.4-py3-none-win_amd64.whl", hash = "sha256:05d9d27a18c4bcbefada602480ec9e01e0bc949d432e0ced5df77edac195919c", size = 10567657, upload-time = "2026-08-20T17:43:54.78Z" },
{ url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, { url = "https://files.pythonhosted.org/packages/0e/fe/da8b9e1347696bb22120b77280ec5ce25d500ca5cb39d5ad6e5c18de19c1/ruff-0.16.4-py3-none-win_arm64.whl", hash = "sha256:a3a61621c9b6f6a89573e938a080e648f1695baa3f58570a3a707bc51ff65a21", size = 10451579, upload-time = "2026-08-20T17:43:57.135Z" },
] ]
[[package]] [[package]]
name = "safety" name = "safety"
version = "3.7.0" version = "3.8.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "authlib" }, { name = "authlib" },
{ name = "certifi" },
{ name = "click" }, { name = "click" },
{ name = "dparse" }, { name = "dparse" },
{ name = "filelock" }, { name = "filelock" },
@@ -1399,17 +1488,17 @@ dependencies = [
{ name = "nltk" }, { name = "nltk" },
{ name = "packaging" }, { name = "packaging" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "requests" },
{ name = "ruamel-yaml" }, { name = "ruamel-yaml" },
{ name = "safety-schemas" }, { name = "safety-schemas" },
{ name = "tenacity" }, { name = "tenacity" },
{ name = "tomlkit" }, { name = "tomlkit" },
{ name = "truststore" },
{ name = "typer" }, { name = "typer" },
{ name = "typing-extensions" }, { name = "typing-extensions" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/6f/e8/1cfffa0d8836de8aa31f4fa7fdeb892c7cfa97cd555039ad5df71ce0e968/safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e", size = 330538, upload-time = "2025-11-06T20:10:15.067Z" } sdist = { url = "https://files.pythonhosted.org/packages/c2/7b/8e1d580c5178f0736b806b7199827e61e2a2569eec5b49ec75da6273bbdf/safety-3.8.1.tar.gz", hash = "sha256:e646123b976bbb6707cfaacae8c926e2f886b744a60e0f410e8610a3a4eaf7be", size = 412947, upload-time = "2026-05-29T15:09:33.355Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/89/55/c4b2058ca346e58124ba082a3596e30dc1f5793710f8173156c7c2d77048/safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf", size = 312436, upload-time = "2025-11-06T20:10:13.576Z" }, { url = "https://files.pythonhosted.org/packages/4b/f5/498db84333a644835e572c0d96cfa705bf9871f863289a7845082d54755e/safety-3.8.1-py3-none-any.whl", hash = "sha256:953c1c3c60c873f53a6cc250b2a9c4b38bb6ef45f0625990e43f20bff916c965", size = 340521, upload-time = "2026-05-29T15:09:31.622Z" },
] ]
[[package]] [[package]]
@@ -1461,28 +1550,23 @@ wheels = [
[[package]] [[package]]
name = "sqlalchemy" name = "sqlalchemy"
version = "2.0.49" version = "2.0.52"
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/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, { url = "https://files.pythonhosted.org/packages/d5/f5/71cb30af58c9b80a4e1fac0b73bb48f86d497a774a6a2eb6d2f1e657bb73/sqlalchemy-2.0.52-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9", size = 2169537, upload-time = "2026-08-11T20:58:13.855Z" },
{ url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, { url = "https://files.pythonhosted.org/packages/4c/93/d07ebd645d1b07b6b5ed63450a70f063a346a7e0f2c8810daf2e532400cb/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e", size = 3319606, upload-time = "2026-08-11T21:02:45.829Z" },
{ url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, { url = "https://files.pythonhosted.org/packages/ae/5c/290c84c7c2566ecd3b65baaae0fddec9bc33b033b398a06123bb86fbfc6e/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437", size = 3323642, upload-time = "2026-08-11T21:17:05.675Z" },
{ url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, { url = "https://files.pythonhosted.org/packages/13/f5/2cc160590ca49173359557880b92a0572293ccb899e8f6cedf150c5a3ddf/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8", size = 3268125, upload-time = "2026-08-11T21:02:47.649Z" },
{ url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, { url = "https://files.pythonhosted.org/packages/35/f3/ea8933fc9f7d1353e9c2ff9965eae687c4cef181120574591ed2fa0633e1/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd", size = 3289516, upload-time = "2026-08-11T21:17:07.267Z" },
{ url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, { url = "https://files.pythonhosted.org/packages/45/67/05cf86541c1e1716fca1e4a996954a439cd74501707cda607fb7cb02ef50/sqlalchemy-2.0.52-cp314-cp314-win32.whl", hash = "sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2", size = 2130249, upload-time = "2026-08-11T21:14:57.273Z" },
{ url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, { url = "https://files.pythonhosted.org/packages/96/d7/8ac6ffa1e36169e762ef65bd835046abb2251b1bc17f8f6708e14ed8d31f/sqlalchemy-2.0.52-cp314-cp314-win_amd64.whl", hash = "sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d", size = 2156718, upload-time = "2026-08-11T21:14:58.667Z" },
{ url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, { url = "https://files.pythonhosted.org/packages/dc/4b/e01a737eef378e734cc6394a82248a6ce13b167dfa36c731075ce9fc9c64/sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582", size = 2190344, upload-time = "2026-08-11T19:53:21.393Z" },
{ url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, { url = "https://files.pythonhosted.org/packages/b3/3f/3582293d1e185e71d19d7c731c3e2ee20ba21981c4a1115c0806c1f62120/sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89", size = 1950700, upload-time = "2026-08-11T20:47:21.603Z" },
{ url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" },
{ url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" },
{ url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" },
{ url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" },
{ url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" },
] ]
[[package]] [[package]]
@@ -1536,6 +1620,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
] ]
[[package]]
name = "truststore"
version = "0.10.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
]
[[package]] [[package]]
name = "typer" name = "typer"
version = "0.24.1" version = "0.24.1"
@@ -1553,11 +1646,11 @@ wheels = [
[[package]] [[package]]
name = "types-openpyxl" name = "types-openpyxl"
version = "3.1.5.20260408" version = "3.1.5.20260807"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/74/c9/24f03f9d9fedd164de699c1418869bef9b819f59f75e7f647f5788c02d98/types_openpyxl-3.1.5.20260408.tar.gz", hash = "sha256:b49274d086fbb6e6bcd2a67d161dd1161d4d380488e4cce546d647d45eadcac2", size = 101361, upload-time = "2026-04-08T04:30:37.809Z" } sdist = { url = "https://files.pythonhosted.org/packages/cd/89/e81814aac1c6ec46ee0006b723254ae737faa68c1ac80a7c3b81b3aa9f22/types_openpyxl-3.1.5.20260807.tar.gz", hash = "sha256:1a0a42b125f8023d3ae83cc057e379d301a87f45e60b6160917824fef28ab015", size = 101740, upload-time = "2026-08-07T04:17:25.557Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/e8/64db0c32c6fda4b198aff84329ceeea00a93d16c28f05e9fe0404ddb8b8c/types_openpyxl-3.1.5.20260408-py3-none-any.whl", hash = "sha256:7ab7586796aed017cde50b81bd67ba024120c39b99c102320b57dd91390c317f", size = 166044, upload-time = "2026-04-08T04:30:36.449Z" }, { url = "https://files.pythonhosted.org/packages/34/50/c7faba803c8a2e822ccc43a091c979cdbdc28e792e1eb0fb5ff172c81ee5/types_openpyxl-3.1.5.20260807-py3-none-any.whl", hash = "sha256:e64e9342cdac8a2d7b09f992d3606c532b75da43874f8107b6b5a122dc9d5681", size = 165826, upload-time = "2026-08-07T04:17:24.341Z" },
] ]
[[package]] [[package]]
@@ -1601,20 +1694,19 @@ wheels = [
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.46.0" version = "0.52.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
{ name = "h11" }, { name = "h11" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/1f/93/041fca8274050e40e6791f267d82e0e2e27dd165627bd640d3e0e378d877/uvicorn-0.46.0.tar.gz", hash = "sha256:fb9da0926999cc6cb22dc7cd71a94a632f078e6ae47ff683c5c420750fb7413d", size = 88758, upload-time = "2026-04-23T07:16:00.151Z" } sdist = { url = "https://files.pythonhosted.org/packages/f2/0f/3f86e61397dd33bf2ccf28188c40db6a740658aeebbbf6e7dbc101a1f487/uvicorn-0.52.4.tar.gz", hash = "sha256:73acfee47a0b133c5de13d219492d62d8a31e935f4fe6e41a232451a15379f86", size = 100627, upload-time = "2026-08-19T06:27:41.821Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, { url = "https://files.pythonhosted.org/packages/f1/79/4a20b54ab0491485ccd8c077db2d39187c7f12b3e15485d38a7be37c81b4/uvicorn-0.52.4-py3-none-any.whl", hash = "sha256:f86e41a149d7d05a9969337e3946a9c171c06a5d42680896daaba624aeac8da1", size = 79871, upload-time = "2026-08-19T06:27:40.36Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
standard = [ standard = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "httptools" }, { name = "httptools" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
{ name = "pyyaml" }, { name = "pyyaml" },
+24 -23
View File
@@ -14,17 +14,17 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^21.1.5", "@angular/animations": "^22.1.2",
"@angular/cdk": "^21.1.5", "@angular/cdk": "^22.1.2",
"@angular/common": "^21.1.5", "@angular/common": "^22.1.2",
"@angular/compiler": "^21.1.5", "@angular/compiler": "^22.1.2",
"@angular/core": "^21.1.5", "@angular/core": "^22.1.2",
"@angular/forms": "^21.1.5", "@angular/forms": "^22.1.2",
"@angular/material": "^21.1.5", "@angular/material": "^22.1.2",
"@angular/material-moment-adapter": "^21.1.5", "@angular/material-moment-adapter": "^22.1.2",
"@angular/platform-browser": "^21.1.5", "@angular/platform-browser": "^22.1.2",
"@angular/platform-browser-dynamic": "^21.1.5", "@angular/platform-browser-dynamic": "^22.1.2",
"@angular/router": "^21.1.5", "@angular/router": "^22.1.2",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
"jspdf": "^4.2.0", "jspdf": "^4.2.0",
"mathjs": "^15.1.1", "mathjs": "^15.1.1",
@@ -33,22 +33,23 @@
"tslib": "^2.6.0" "tslib": "^2.6.0"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^21.1.4", "@angular/build": "^22.1.4",
"@angular/cli": "^21.1.4", "@angular/cli": "^22.1.4",
"@angular/compiler-cli": "^21.1.5", "@angular/compiler-cli": "^22.1.2",
"@angular/language-service": "^21.1.5", "@angular/language-service": "^22.1.2",
"angular-eslint": "^21.2.0", "@eslint/js": "10.0.1",
"eslint": "^9.28.0", "angular-eslint": "^22.1.0",
"eslint": "^10.8.1",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-perfectionist": "^4.15.0", "eslint-plugin-perfectionist": "^5.10.1",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^14.0.0",
"eslint-plugin-unused-imports": "^4.1.4", "eslint-plugin-unused-imports": "^4.1.4",
"husky": "^9.0.11", "husky": "^9.0.11",
"lint-staged": "^16.2.7", "lint-staged": "^17.3.0",
"prettier": "^3.8.1", "prettier": "^3.9.6",
"typescript": "~5.9.3", "typescript": "~6.0.3",
"typescript-eslint": "^8.33.1" "typescript-eslint": "^8.67.0"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
@@ -1,54 +1,61 @@
<h2 class="row-container space-between wrapped"> <h2 class="row-container space-between wrapped">
<span>Account</span> <span>Account</span>
<mat-icon matSuffix (click)="item.isStarred = !item.isStarred" class="pointer" [class.gold]="item.isStarred"> <mat-icon matSuffix (click)="item().isStarred = !item().isStarred" class="pointer" [class.gold]="item().isStarred">
{{ item.isStarred ? 'star' : 'star_border' }} {{ item().isStarred ? 'star' : 'star_border' }}
</mat-icon> </mat-icon>
</h2> </h2>
<form [formGroup]="form" class="flex-col wrapped"> @if (itemResource.isLoading()) {
<app-skeleton-loader type="card" [count]="1" />
<app-skeleton-loader type="line" [count]="10" />
} @else if (itemResource.error()) {
<app-error-state [message]="'Failed to load data.'" (retryAction)="itemResource.reload()" />
} @else {
<form class="flex-col wrapped" [formRoot]="accountForm">
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Code</mat-label>
<input matInput [formField]="accountForm.code" />
</mat-form-field>
</div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Name</mat-label>
<input matInput [formField]="accountForm.name" />
</mat-form-field>
</div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Account Type</mat-label>
<mat-select [formField]="accountForm.type">
@for (at of accountTypes(); track at) {
<mat-option [value]="at.id">
{{ at.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="row-container">
<mat-checkbox [formField]="accountForm.isActive" class="flex-auto">Is Active?</mat-checkbox>
<mat-checkbox [formField]="accountForm.isReconcilable" class="flex-auto">Is Reconcilable?</mat-checkbox>
</div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Cost Centre</mat-label>
<mat-select [formField]="accountForm.costCentre">
@for (cs of costCentres(); track cs) {
<mat-option [value]="cs.id">
{{ cs.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</form>
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <button mat-raised-button color="primary" (click)="save()" class="">Save</button>
<mat-label>Code</mat-label> @if (!!item().id) {
<input matInput formControlName="code" /> <button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
</mat-form-field> }
</div> </div>
<div class="row-container"> }
<mat-form-field class="flex-auto">
<mat-label>Name</mat-label>
<input matInput #nameElement formControlName="name" />
</mat-form-field>
</div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Account Type</mat-label>
<mat-select formControlName="type">
@for (at of accountTypes; track at) {
<mat-option [value]="at.id">
{{ at.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="row-container">
<mat-checkbox formControlName="isActive" class="flex-auto">Is Active?</mat-checkbox>
<mat-checkbox formControlName="isReconcilable" class="flex-auto">Is Reconcilable?</mat-checkbox>
</div>
<div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Cost Centre</mat-label>
<mat-select formControlName="costCentre">
@for (cs of costCentres; track cs) {
<mat-option [value]="cs.id">
{{ cs.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</form>
<div class="row-container">
<button mat-raised-button color="primary" (click)="save()" class="">Save</button>
@if (!!item.id) {
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
}
</div>
@@ -1,5 +1,5 @@
import { AfterViewInit, Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'; import { Component, inject, afterNextRender, linkedSignal, input, computed } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
@@ -9,13 +9,24 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router'; import { Router } from '@angular/router';
import { Account } from '../../core/account'; import { Account } from '../../core/account';
import { AccountType } from '../../core/account-type';
import { AccountService } from '../../core/account.service'; import { AccountService } from '../../core/account.service';
import { CostCentre } from '../../core/cost-centre'; import { CostCentreService } from '../../cost-centre/cost-centre.service';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
import { AccountTypeService } from '../account-type.service';
export interface AccountDetailFormData {
code: string;
name: string;
type: number;
isActive: boolean;
isReconcilable: boolean;
costCentre: string;
}
@Component({ @Component({
selector: 'app-account-detail', selector: 'app-account-detail',
@@ -23,79 +34,59 @@ import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dial
styleUrls: ['./account-detail.component.css'], styleUrls: ['./account-detail.component.css'],
imports: [ imports: [
MatIconModule, MatIconModule,
ReactiveFormsModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatSelectModule, MatSelectModule,
MatOptionModule, MatOptionModule,
MatCheckboxModule, MatCheckboxModule,
MatButtonModule, MatButtonModule,
FormField,
FormRoot,
SkeletonLoaderComponent,
ErrorStateComponent,
], ],
}) })
export class AccountDetailComponent implements OnInit, AfterViewInit { export class AccountDetailComponent {
private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
private ser = inject(AccountService); private ser = inject(AccountService);
private accountTypeService = inject(AccountTypeService);
private costCentreService = inject(CostCentreService);
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
@ViewChild('nameElement', { static: true }) nameElement!: ElementRef<HTMLInputElement>; itemResource = this.ser.get(this.id);
form: FormGroup<{
code: FormControl<number | string>;
name: FormControl<string | null>;
type: FormControl<number>;
isActive: FormControl<boolean>;
isReconcilable: FormControl<boolean>;
costCentre: FormControl<string | null>;
}>;
accountTypes: AccountType[] = []; item = computed(() => this.itemResource.value() ?? new Account());
costCentres: CostCentre[] = [];
item: Account = new Account(); accountModel = linkedSignal({
source: this.item,
computation: (itemVal): AccountDetailFormData => {
return {
code: (itemVal.code || '(Auto)').toString(),
name: itemVal.name || '',
type: itemVal.type ?? 0,
isActive: itemVal.isActive ?? true,
isReconcilable: itemVal.isReconcilable ?? false,
costCentre: itemVal.costCentre?.id ?? '',
};
},
});
accountForm = form(this.accountModel);
accountTypesResource = this.accountTypeService.list();
costCentresResource = this.costCentreService.list();
accountTypes = computed(() => this.accountTypesResource.value() || []);
costCentres = computed(() => this.costCentresResource.value() || []);
constructor() { constructor() {
this.form = new FormGroup({ afterNextRender(() => {
code: new FormControl<string | number>({ value: 0, disabled: true }, { nonNullable: true }), this.accountForm.name().focusBoundControl();
name: new FormControl<string | null>(null),
type: new FormControl<number>(0, { nonNullable: true }),
isActive: new FormControl<boolean>(true, { nonNullable: true }),
isReconcilable: new FormControl<boolean>(false, { nonNullable: true }),
costCentre: new FormControl<string | null>(null),
}); });
} }
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as {
item: Account;
accountTypes: AccountType[];
costCentres: CostCentre[];
};
this.accountTypes = data.accountTypes;
this.costCentres = data.costCentres;
this.showItem(data.item);
});
}
showItem(item: Account) {
this.item = item;
this.form.setValue({
code: this.item.code || '(Auto)',
name: this.item.name || '',
type: this.item.type,
isActive: this.item.isActive,
isReconcilable: this.item.isReconcilable,
costCentre: this.item.costCentre.id ?? '',
});
}
ngAfterViewInit() {
setTimeout(() => {
this.nameElement.nativeElement.focus();
}, 0);
}
save() { save() {
this.ser.saveOrUpdate(this.getItem()).subscribe({ this.ser.saveOrUpdate(this.getItem()).subscribe({
next: () => { next: () => {
@@ -103,19 +94,19 @@ export class AccountDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/accounts'); this.router.navigateByUrl('/accounts');
}, },
error: (error) => { error: (error) => {
this.snackBar.open(error, 'Danger'); this.snackBar.open(error as string, 'Danger');
}, },
}); });
} }
delete() { delete() {
this.ser.delete(this.item.id as string).subscribe({ this.ser.delete(this.item().id as string).subscribe({
next: () => { next: () => {
this.snackBar.open('', 'Success'); this.snackBar.open('', 'Success');
this.router.navigateByUrl('/accounts'); this.router.navigateByUrl('/accounts');
}, },
error: (error) => { error: (error) => {
this.snackBar.open(error, 'Danger'); this.snackBar.open(error as string, 'Danger');
}, },
}); });
} }
@@ -134,12 +125,13 @@ export class AccountDetailComponent implements OnInit, AfterViewInit {
} }
getItem(): Account { getItem(): Account {
const formModel = this.form.value; const formModel = this.accountModel();
this.item.name = formModel.name ?? ''; const item = this.item();
this.item.type = formModel.type ?? 0; item.name = formModel.name ?? '';
this.item.isActive = formModel.isActive ?? true; item.type = formModel.type ?? 0;
this.item.isReconcilable = formModel.isReconcilable ?? false; item.isActive = formModel.isActive ?? true;
this.item.costCentre.id = formModel.costCentre ?? ''; item.isReconcilable = formModel.isReconcilable ?? false;
return this.item; item.costCentre.id = formModel.costCentre ?? '';
return item;
} }
} }
@@ -1,18 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { Account } from '../core/account';
import { accountListResolver } from './account-list.resolver';
describe('accountListResolver', () => {
const executeResolver: ResolveFn<Account[]> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => accountListResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});
@@ -1,9 +0,0 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { Account } from '../core/account';
import { AccountService } from '../core/account.service';
export const accountListResolver: ResolveFn<Account[]> = () => {
return inject(AccountService).list();
};
@@ -1,101 +0,0 @@
import { DataSource } from '@angular/cdk/collections';
import { EventEmitter } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort, Sort } from '@angular/material/sort';
import { merge, Observable, of as observableOf } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { Account } from '../../core/account';
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
const compare = (a: string | number | boolean, b: string | number | boolean, isAsc: boolean) =>
(a < b ? -1 : 1) * (isAsc ? 1 : -1);
export class AccountListDataSource extends DataSource<Account> {
private filterValue = '';
constructor(
public data: Account[],
private filter: Observable<string>,
private paginator?: MatPaginator,
private sort?: MatSort,
) {
super();
this.filter = filter.pipe(
tap((x) => {
this.filterValue = x;
}),
);
}
connect(): Observable<Account[]> {
const dataMutations: (EventEmitter<PageEvent> | EventEmitter<Sort>)[] = [];
if (this.paginator) {
dataMutations.push((this.paginator as MatPaginator).page);
}
if (this.sort) {
dataMutations.push((this.sort as MatSort).sortChange);
}
return merge(observableOf(this.data), this.filter, ...dataMutations)
.pipe(
map(() => this.getFilteredData([...this.data])),
tap((x: Account[]) => {
if (this.paginator) {
this.paginator.length = x.length;
}
}),
)
.pipe(map((x: Account[]) => this.getPagedData(this.getSortedData(x))));
}
disconnect() {}
private getFilteredData(data: Account[]): Account[] {
return this.filterValue.split(' ').reduce(
(p: Account[], c: string) =>
p.filter((x) => {
const accountString = `${x.name} ${x.type} ${x.costCentre}${
x.isActive ? ' active' : ' inactive'
}`.toLowerCase();
return accountString.indexOf(c) !== -1;
}),
Object.assign([], data),
);
}
private getPagedData(data: Account[]) {
if (this.paginator === undefined) {
return data;
}
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
return data.splice(startIndex, this.paginator.pageSize);
}
private getSortedData(data: Account[]) {
if (this.sort === undefined) {
return data;
}
if (!this.sort.active || this.sort.direction === '') {
return data;
}
const sort = this.sort as MatSort;
return data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'name':
return compare(a.name, b.name, isAsc);
case 'type':
return compare(a.typeName, b.typeName, isAsc);
case 'isActive':
return compare(a.isActive, b.isActive, isAsc);
case 'isReconcilable':
return compare(a.isReconcilable, b.isReconcilable, isAsc);
case 'costCentre':
return compare(a.costCentre.name, b.costCentre.name, isAsc);
default:
return 0;
}
});
}
}
@@ -6,61 +6,67 @@
</a> </a>
</h2> </h2>
<form [formGroup]="form" class="flex-col"> <form class="flex-col" [formRoot]="accountForm">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Filter</mat-label> <mat-label>Filter</mat-label>
<input type="text" matInput #filterElement formControlName="filter" autocomplete="off" /> <input type="text" matInput #filterElement [formField]="accountForm.filter" autocomplete="off" />
</mat-form-field> </mat-form-field>
</div> </div>
</form> </form>
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements"> @if (listResource.isLoading()) {
<!-- Name Column --> <app-skeleton-loader type="table" [count]="1" />
<ng-container matColumnDef="name"> } @else if (listResource.error()) {
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell> <app-error-state [message]="'Failed to load data.'" (retryAction)="listResource.reload()" />
<mat-cell *matCellDef="let row"> } @else {
<a [routerLink]="['/accounts', row.id]"> <mat-table #table [dataSource]="dataSource()" matSort (matSortChange)="sortData($event)">
@if (row.isStarred) { <!-- Name Column -->
<mat-icon matSuffix class="gold">star </mat-icon> <ng-container matColumnDef="name">
} <mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
{{ row.name }} <mat-cell *matCellDef="let row">
</a> <a [routerLink]="['/accounts', row.id]">
</mat-cell> @if (row.isStarred) {
</ng-container> <mat-icon matSuffix class="gold">star </mat-icon>
}
{{ row.name }}
</a>
</mat-cell>
</ng-container>
<!-- Type Column --> <!-- Type Column -->
<ng-container matColumnDef="type"> <ng-container matColumnDef="type">
<mat-header-cell *matHeaderCellDef mat-sort-header>Type</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Type</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.typeName }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.typeName }}</mat-cell>
</ng-container> </ng-container>
<!-- Is Active? Column --> <!-- Is Active? Column -->
<ng-container matColumnDef="isActive"> <ng-container matColumnDef="isActive">
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Active?</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Is Active?</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.isActive }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.isActive }}</mat-cell>
</ng-container> </ng-container>
<!-- Is Reconcilable? Column --> <!-- Is Reconcilable? Column -->
<ng-container matColumnDef="isReconcilable"> <ng-container matColumnDef="isReconcilable">
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Reconcilable?</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Is Reconcilable?</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.isReconcilable }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.isReconcilable }}</mat-cell>
</ng-container> </ng-container>
<!-- Cost Centre Column --> <!-- Cost Centre Column -->
<ng-container matColumnDef="costCentre"> <ng-container matColumnDef="costCentre">
<mat-header-cell *matHeaderCellDef mat-sort-header>Cost Centre</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Cost Centre</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.costCentre.name }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.costCentre.name }}</mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row> <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table> </mat-table>
<mat-paginator <mat-paginator
#paginator (page)="handlePageEvent($event)"
[length]="dataSource.data.length" [length]="filteredList().length"
[pageIndex]="0" [pageIndex]="pageIndex()"
[pageSize]="50" [pageSize]="pageSize()"
[pageSizeOptions]="[25, 50, 100, 250]" [pageSizeOptions]="[25, 50, 100, 250]"
> >
</mat-paginator> </mat-paginator>
}
@@ -1,77 +1,174 @@
import { AfterViewInit, Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'; import {
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; Component,
inject,
signal,
effect,
computed,
debounced,
input,
linkedSignal,
afterNextRender,
} from '@angular/core';
import { form, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { Account } from '../../core/account'; import { Account } from '../../core/account';
import { AccountType } from '../../core/account-type'; import { AccountService } from '../../core/account.service';
import { AccountListDataSource } from './account-list-datasource'; import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
import { AccountTypeService } from '../account-type.service';
export interface AccountListFormData {
filter: string;
}
@Component({ @Component({
selector: 'app-account-list', selector: 'app-account-list',
templateUrl: './account-list.component.html', templateUrl: './account-list.component.html',
styleUrls: ['./account-list.component.css'], styleUrls: ['./account-list.component.css'],
imports: [ imports: [
RouterModule, RouterLink,
MatButtonModule, MatButtonModule,
MatIconModule, MatIconModule,
ReactiveFormsModule, FormField,
FormRoot,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatTableModule, MatTableModule,
MatSortModule, MatSortModule,
MatPaginatorModule, MatPaginatorModule,
SkeletonLoaderComponent,
ErrorStateComponent,
], ],
}) })
export class AccountListComponent implements OnInit, AfterViewInit { export class AccountListComponent {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router);
private ser = inject(AccountService);
private accountTypeSer = inject(AccountTypeService);
pageSize = signal(50);
pageIndex = signal(0);
sortActive = signal('');
sortDirection = signal('');
q = input('', { transform: (v: string | null | undefined) => v ?? '' });
formModel = linkedSignal({
source: this.q,
computation: (qValue): AccountListFormData => ({ filter: qValue }),
});
@ViewChild('filterElement', { static: true }) filterElement!: ElementRef<HTMLInputElement>; accountForm = form(this.formModel);
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
@ViewChild(MatSort, { static: true }) sort!: MatSort;
dataSource: AccountListDataSource;
filter: Observable<string>;
form: FormGroup<{
filter: FormControl<string>;
}>;
list: Account[] = []; filterSignal = computed(() => this.formModel().filter);
accountTypes: AccountType[] = [];
debouncedFilter = debounced(this.filterSignal, 150);
listResource = this.ser.list();
accountTypesResource = this.accountTypeSer.list();
accountTypes = computed(() => this.accountTypesResource.value() ?? []);
list = computed(() => this.listResource.value() ?? []);
listWithTypes = computed(() => {
const data = this.list();
const types = this.accountTypes();
return data.map(
(account) =>
new Account({
...account,
typeName: types.find((t) => t.id === account.type)?.name ?? account.type.toString(),
}),
);
});
filteredList = computed(() => {
const q = (this.debouncedFilter.value() ?? '').trim().toLowerCase();
const data = this.listWithTypes();
if (!q) {
return data;
}
return q.split(' ').reduce(
(p: Account[], c: string) =>
p.filter((x) => {
const accountString = `${x.name} ${x.type} ${x.costCentre}${
x.isActive ? ' active' : ' inactive'
}`.toLowerCase();
return accountString.indexOf(c) !== -1;
}),
[...data],
);
});
sortedList = computed(() => {
const data = this.filteredList();
const active = this.sortActive();
const direction = this.sortDirection();
if (!active || direction === '') {
return data;
}
return [...data].sort((a, b) => {
const isAsc = direction === 'asc';
switch (active) {
case 'name':
return compare(a.name, b.name, isAsc);
case 'type':
return compare(a.typeName, b.typeName, isAsc);
case 'isActive':
return compare(a.isActive, b.isActive, isAsc);
case 'isReconcilable':
return compare(a.isReconcilable, b.isReconcilable, isAsc);
case 'costCentre':
return compare(a.costCentre.name, b.costCentre.name, isAsc);
default:
return 0;
}
});
});
dataSource = computed(() => {
const data = this.sortedList();
const pageIndex = this.pageIndex();
const pageSize = this.pageSize();
return data.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
});
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['name', 'type', 'isActive', 'isReconcilable', 'costCentre']; displayedColumns = ['name', 'type', 'isActive', 'isReconcilable', 'costCentre'];
constructor() { constructor() {
this.form = new FormGroup({ afterNextRender(() => {
filter: new FormControl<string>('', { nonNullable: true }), this.accountForm().focusBoundControl();
});
effect(() => {
const q = this.debouncedFilter.value() ?? '';
this.router.navigate([], {
relativeTo: this.route,
queryParams: { q: q || null },
queryParamsHandling: 'merge',
replaceUrl: true,
});
}); });
// Listen to Filter Change
this.filter = this.form.controls.filter.valueChanges.pipe(debounceTime(150), distinctUntilChanged());
this.dataSource = new AccountListDataSource(this.list, this.filter, this.paginator, this.sort);
} }
ngOnInit() { handlePageEvent(e: PageEvent) {
this.route.data.subscribe((value) => { this.pageSize.set(e.pageSize);
const data = value as { list: Account[]; accountTypes: AccountType[] }; this.pageIndex.set(e.pageIndex);
this.accountTypes = data.accountTypes;
data.list.forEach((x) => (x.typeName = this.accountTypes.find((y) => y.id === x.type)?.name ?? ''));
this.list = data.list;
});
this.dataSource = new AccountListDataSource(this.list, this.filter, this.paginator, this.sort);
} }
ngAfterViewInit() { sortData(sort: Sort) {
setTimeout(() => { this.sortActive.set(sort.active);
this.filterElement.nativeElement.focus(); this.sortDirection.set(sort.direction);
}, 0);
} }
} }
const compare = (a: string | number | boolean, b: string | number | boolean, isAsc: boolean) =>
(a < b ? -1 : 1) * (isAsc ? 1 : -1);
@@ -1,18 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { AccountType } from '../core/account-type';
import { accountTypeResolver } from './account-type.resolver';
describe('accountTypeResolver', () => {
const executeResolver: ResolveFn<AccountType[]> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => accountTypeResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});
@@ -1,9 +0,0 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { AccountType } from '../core/account-type';
import { AccountTypeService } from './account-type.service';
export const accountTypeResolver: ResolveFn<AccountType[]> = () => {
return inject(AccountTypeService).list();
};
@@ -1,14 +1,10 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient, httpResource } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
import { AccountType } from '../core/account-type'; import { AccountType } from '../core/account-type';
import { ErrorLoggerService } from '../core/error-logger.service'; import { ErrorLoggerService } from '../core/error-logger.service';
const url = '/api/account-types'; const url = '/api/account-types';
const serviceName = 'AccountTypeService';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@@ -16,9 +12,7 @@ export class AccountTypeService {
private http = inject(HttpClient); private http = inject(HttpClient);
private log = inject(ErrorLoggerService); private log = inject(ErrorLoggerService);
list(): Observable<AccountType[]> { list() {
return this.http.get<AccountType[]>(url).pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable< return httpResource<AccountType[]>(() => url);
AccountType[]
>;
} }
} }
@@ -1,18 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { Account } from '../core/account';
import { accountResolver } from './account.resolver';
describe('accountResolver', () => {
const executeResolver: ResolveFn<Account> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => accountResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});
@@ -1,10 +0,0 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { Account } from '../core/account';
import { AccountService } from '../core/account.service';
export const accountResolver: ResolveFn<Account> = (route) => {
const id = route.paramMap.get('id');
return inject(AccountService).get(id);
};
@@ -1,12 +1,8 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service'; import { authGuard } from '../auth/auth-guard.service';
import { costCentreListResolver } from '../cost-centre/cost-centre-list.resolver';
import { AccountDetailComponent } from './account-detail/account-detail.component'; import { AccountDetailComponent } from './account-detail/account-detail.component';
import { accountListResolver } from './account-list.resolver';
import { AccountListComponent } from './account-list/account-list.component'; import { AccountListComponent } from './account-list/account-list.component';
import { accountTypeResolver } from './account-type.resolver';
import { accountResolver } from './account.resolver';
export const routes: Routes = [ export const routes: Routes = [
{ {
@@ -16,10 +12,6 @@ export const routes: Routes = [
data: { data: {
permission: 'Accounts', permission: 'Accounts',
}, },
resolve: {
list: accountListResolver,
accountTypes: accountTypeResolver,
},
}, },
{ {
path: 'new', path: 'new',
@@ -28,11 +20,6 @@ export const routes: Routes = [
data: { data: {
permission: 'Accounts', permission: 'Accounts',
}, },
resolve: {
item: accountResolver,
accountTypes: accountTypeResolver,
costCentres: costCentreListResolver,
},
}, },
{ {
path: ':id', path: ':id',
@@ -41,10 +28,5 @@ export const routes: Routes = [
data: { data: {
permission: 'Accounts', permission: 'Accounts',
}, },
resolve: {
item: accountResolver,
accountTypes: accountTypeResolver,
costCentres: costCentreListResolver,
},
}, },
]; ];
+21 -37
View File
@@ -1,60 +1,44 @@
import { LayoutModule } from '@angular/cdk/layout'; import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideHttpClient, withInterceptors, withFetch } from '@angular/common/http'; import {
import { ApplicationConfig, importProvidersFrom, LOCALE_ID, provideZonelessChangeDetection } from '@angular/core'; ApplicationConfig,
import { ReactiveFormsModule } from '@angular/forms'; importProvidersFrom,
isDevMode,
LOCALE_ID,
provideZonelessChangeDetection,
} from '@angular/core';
import { MomentDateAdapter } from '@angular/material-moment-adapter'; import { MomentDateAdapter } from '@angular/material-moment-adapter';
import { MatButtonModule } from '@angular/material/button';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core'; import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MatDividerModule } from '@angular/material/divider'; import { MAT_SNACK_BAR_DEFAULT_OPTIONS } from '@angular/material/snack-bar';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MAT_SNACK_BAR_DEFAULT_OPTIONS, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatToolbarModule } from '@angular/material/toolbar';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { provideRouter, withRouterConfig } from '@angular/router'; import { provideRouter, withComponentInputBinding, withRouterConfig } from '@angular/router';
import { dateFormat } from './app.environment'; import { dateFormat } from './app.environment';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { AuthService } from './auth/auth.service'; import { AuthService } from './auth/auth.service';
import { authInterceptor } from './core/auth.interceptor'; import { authInterceptor } from './core/auth.interceptor';
import { delayInterceptor } from './core/delay-interceptor';
import { jwtInterceptor } from './core/jwt.interceptor'; import { jwtInterceptor } from './core/jwt.interceptor';
import { refreshInterceptor } from './core/refresh.interceptor'; import { refreshInterceptor } from './core/refresh.interceptor';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
importProvidersFrom( importProvidersFrom(BrowserModule),
BrowserModule,
LayoutModule,
MatButtonModule,
MatDividerModule,
MatIconModule,
MatInputModule,
MatSidenavModule,
MatToolbarModule,
MatListModule,
MatMenuModule,
MatProgressSpinnerModule,
MatSnackBarModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
ReactiveFormsModule,
),
{ provide: LOCALE_ID, useValue: 'en-IN' }, { provide: LOCALE_ID, useValue: 'en-IN' },
AuthService, AuthService,
provideHttpClient(withFetch(), withInterceptors([refreshInterceptor, jwtInterceptor, authInterceptor])), provideHttpClient(
withInterceptors([
authInterceptor,
jwtInterceptor,
refreshInterceptor,
...(isDevMode() ? [delayInterceptor] : []),
]),
),
provideRouter( provideRouter(
routes, routes,
withRouterConfig({ withRouterConfig({
onSameUrlNavigation: 'reload', onSameUrlNavigation: 'reload',
}), }),
withComponentInputBinding(),
), ),
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] }, { provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
{ provide: MAT_DATE_FORMATS, useValue: dateFormat }, { provide: MAT_DATE_FORMATS, useValue: dateFormat },
@@ -1,16 +0,0 @@
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs';
import { AttendanceItem } from './attendance-item';
export class AttendanceDataSource extends DataSource<AttendanceItem> {
constructor(private data: Observable<AttendanceItem[]>) {
super();
}
connect(): Observable<AttendanceItem[]> {
return this.data;
}
disconnect() {}
}
@@ -1,9 +0,0 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { AttendanceType } from './attendance-type';
import { AttendanceTypeService } from './attendance-type.service';
export const attendanceTypeResolver: ResolveFn<AttendanceType[]> = () => {
return inject(AttendanceTypeService).list();
};
@@ -1,22 +1,16 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient, httpResource } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from '../core/error-logger.service'; import { ErrorLoggerService } from '../core/error-logger.service';
import { AttendanceType } from './attendance-type'; import { AttendanceType } from './attendance-type';
const url = '/api/attendance-types'; const url = '/api/attendance-types';
const serviceName = 'AttendanceTypeService';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AttendanceTypeService { export class AttendanceTypeService {
private http = inject(HttpClient); private http = inject(HttpClient);
private log = inject(ErrorLoggerService); private log = inject(ErrorLoggerService);
list(): Observable<AttendanceType[]> { list() {
return this.http return httpResource<AttendanceType[]>(() => url);
.get<AttendanceType[]>(url)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<AttendanceType[]>;
} }
} }
@@ -1,6 +1,6 @@
<h2>Attendance</h2> <h2>Attendance</h2>
<form [formGroup]="form" class="flex-col"> <form class="flex-col" [formRoot]="form">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-4-5"> <mat-form-field class="flex-auto basis-4-5">
<mat-label>Date</mat-label> <mat-label>Date</mat-label>
@@ -9,7 +9,7 @@
#dateElement #dateElement
[matDatepicker]="date" [matDatepicker]="date"
(focus)="dateElement.select()" (focus)="dateElement.select()"
formControlName="date" [formField]="form.date"
autocomplete="off" autocomplete="off"
/> />
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
@@ -17,73 +17,81 @@
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button> <button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button>
</div> </div>
<mat-table #table [dataSource]="dataSource" formArrayName="attendances"> @if (infoResource.isLoading()) {
<!-- Code Column --> <app-skeleton-loader type="table" [count]="1" />
<ng-container matColumnDef="code"> } @else if (infoResource.error()) {
<mat-header-cell *matHeaderCellDef class="right">Code</mat-header-cell> <app-error-state [message]="'Failed to load data.'" (retryAction)="infoResource.reload()" />
<mat-cell *matCellDef="let row" class="right">{{ row.code }}</mat-cell> } @else {
</ng-container> <mat-table #table [dataSource]="info().body">
<!-- Code Column -->
<ng-container matColumnDef="code">
<mat-header-cell *matHeaderCellDef class="right">Code</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.code }}</mat-cell>
</ng-container>
<!-- Name Column --> <!-- Name Column -->
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell> <mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.name }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.name }}</mat-cell>
</ng-container> </ng-container>
<!-- Designation Column --> <!-- Designation Column -->
<ng-container matColumnDef="designation"> <ng-container matColumnDef="designation">
<mat-header-cell *matHeaderCellDef>Designation</mat-header-cell> <mat-header-cell *matHeaderCellDef>Designation</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.designation }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.designation }}</mat-cell>
</ng-container> </ng-container>
<!-- Department Column --> <!-- Department Column -->
<ng-container matColumnDef="department"> <ng-container matColumnDef="department">
<mat-header-cell *matHeaderCellDef>Department</mat-header-cell> <mat-header-cell *matHeaderCellDef>Department</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.department }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.department }}</mat-cell>
</ng-container> </ng-container>
<!-- Status Column --> <!-- Status Column -->
<ng-container matColumnDef="status"> <ng-container matColumnDef="status">
<mat-header-cell *matHeaderCellDef>Status</mat-header-cell> <mat-header-cell *matHeaderCellDef>Status</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="flex-auto"> <mat-cell *matCellDef="let row; let i = index" class="flex-auto">
<mat-select formControlName="attendanceType" name="attendanceType"> @if (form.attendances[i]) {
@for (at of attendanceTypes; track at) { <mat-select [formField]="form.attendances[i].attendanceType">
<mat-option [value]="at.id"> @for (at of attendanceTypes(); track at) {
{{ at.name }} <mat-option [value]="at.id">
</mat-option> {{ at.name }}
</mat-option>
}
</mat-select>
} }
</mat-select> </mat-cell>
</mat-cell> </ng-container>
</ng-container>
<!-- Prints Column --> <!-- Prints Column -->
<ng-container matColumnDef="prints"> <ng-container matColumnDef="prints">
<mat-header-cell *matHeaderCellDef>Prints</mat-header-cell> <mat-header-cell *matHeaderCellDef>Prints</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" class="no-bg"> <mat-cell *matCellDef="let row; let i = index" class="no-bg">
{{ row.prints }} {{ row.prints }}
@if (!form.controls.attendances.controls[i].controls.attendanceType.pristine) { @if (form.attendances[i] && form.attendances[i].attendanceType !== null) {
<mat-icon class="no-bg">new_releases</mat-icon> <mat-icon class="no-bg">new_releases</mat-icon>
}
<mat-chip-set class="no-bg">
@if (row.hoursWorked.length) {
<mat-chip-option
class="no-bg"
selected
selectable="false"
[color]="row.fullDay === true ? 'primary' : 'warn'"
>
{{ row.hoursWorked }}
</mat-chip-option>
} }
</mat-chip-set> <mat-chip-set class="no-bg">
</mat-cell> @if (row.hoursWorked.length) {
</ng-container> <mat-chip-option
class="no-bg"
selected
selectable="false"
[color]="row.fullDay === true ? 'primary' : 'warn'"
>
{{ row.hoursWorked }}
</mat-chip-option>
}
</mat-chip-set>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; let i = index; columns: displayedColumns" [class]="getClass(i)"></mat-row> <mat-row *matRowDef="let row; let i = index; columns: displayedColumns" [class]="getClass(i)"></mat-row>
</mat-table> </mat-table>
}
</form> </form>
<div class="row-container"> <div class="row-container">
<button mat-raised-button color="primary" (click)="save()" [disabled]="form.pristine">Save</button> <button mat-raised-button color="primary" (click)="save()">Save</button>
</div> </div>
@@ -1,5 +1,5 @@
import { Component, ElementRef, HostListener, inject, OnInit, ViewChild } from '@angular/core'; import { Component, HostListener, inject, input, computed, linkedSignal, afterNextRender } from '@angular/core';
import { FormArray, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips'; import { MatChipsModule } from '@angular/material/chips';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
@@ -11,23 +11,28 @@ import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { Router } from '@angular/router';
import moment from 'moment'; import moment from 'moment';
import { BehaviorSubject } from 'rxjs';
import { AuthService } from '../auth/auth.service'; import { AuthService } from '../auth/auth.service';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { Attendance } from './attendance'; import { Attendance } from './attendance';
import { AttendanceDataSource } from './attendance-datasource'; import { AttendanceTypeService } from './attendance-type.service';
import { AttendanceItem } from './attendance-item';
import { AttendanceType } from './attendance-type';
import { AttendanceService } from './attendance.service'; import { AttendanceService } from './attendance.service';
export interface AttendanceFormData {
date: moment.Moment;
attendances: { attendanceType: number | null }[];
}
@Component({ @Component({
selector: 'app-attendance', selector: 'app-attendance',
templateUrl: './attendance.component.html', templateUrl: './attendance.component.html',
styleUrls: ['./attendance.component.css'], styleUrls: ['./attendance.component.css'],
imports: [ imports: [
ReactiveFormsModule, FormField,
FormRoot,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatDatepickerModule, MatDatepickerModule,
@@ -37,76 +42,62 @@ import { AttendanceService } from './attendance.service';
MatOptionModule, MatOptionModule,
MatIconModule, MatIconModule,
MatChipsModule, MatChipsModule,
SkeletonLoaderComponent,
ErrorStateComponent,
], ],
}) })
export class AttendanceComponent implements OnInit { export class AttendanceComponent {
private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
private auth = inject(AuthService); private auth = inject(AuthService);
private ser = inject(AttendanceService); private ser = inject(AttendanceService);
private attendanceTypeSer = inject(AttendanceTypeService);
date = input(null, { transform: (v: string | null | undefined) => v ?? null });
@ViewChild('dateElement', { static: false }) dateElement!: ElementRef<HTMLInputElement>; infoResource = this.ser.get(this.date);
public attendanceObservable = new BehaviorSubject<AttendanceItem[]>([]); info = computed(() => this.infoResource.value() ?? new Attendance());
dataSource: AttendanceDataSource = new AttendanceDataSource(this.attendanceObservable); attendanceTypesResource = this.attendanceTypeSer.list();
form: FormGroup<{ attendanceTypes = computed(() => this.attendanceTypesResource.value() ?? []);
date: FormControl<moment.Moment>;
attendances: FormArray<
FormGroup<{
attendanceType: FormControl<number>;
}>
>;
}>;
info: Attendance = new Attendance(); model = linkedSignal({
attendanceTypes: AttendanceType[] = []; source: this.info,
computation: (infoVal): AttendanceFormData => ({
date: moment(infoVal.date),
attendances: infoVal.body.map((x) => ({
attendanceType: x.attendanceType.id,
})),
}),
});
form = createForm(this.model);
displayedColumns = ['code', 'name', 'designation', 'department', 'status', 'prints']; displayedColumns = ['code', 'name', 'designation', 'department', 'status', 'prints'];
@HostListener('window:keydown.f2', ['$event']) @HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.dateElement.nativeElement.focus(); this.form().focusBoundControl();
this.dateElement.nativeElement.select();
} }
constructor() { constructor() {
this.form = new FormGroup({ afterNextRender(() => {
date: new FormControl(moment(new Date()), { nonNullable: true }), this.form().focusBoundControl();
attendances: new FormArray<FormGroup<{ attendanceType: FormControl<number> }>>([]),
});
}
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { info: Attendance; attendanceTypes: AttendanceType[] };
this.info = data.info;
this.attendanceTypes = data.attendanceTypes;
this.form.controls.date.setValue(moment(this.info.date, 'DD-MMM-YYYY'));
this.form.controls.attendances.clear();
this.info.body.forEach((x) =>
this.form.controls.attendances.push(
new FormGroup({
attendanceType: new FormControl(x.attendanceType.id, { nonNullable: true }),
}),
),
);
this.dataSource = new AttendanceDataSource(this.attendanceObservable);
this.attendanceObservable.next(this.info.body);
}); });
} }
getClass(index: number) { getClass(index: number) {
const array = this.form.controls.attendances; const array = this.model().attendances;
const id = array.controls[index].value.attendanceType; if (!array || !array[index]) return '';
const { name } = this.attendanceTypes.filter((x) => x.id === id)[0]; const id = array[index].attendanceType;
return name.toLowerCase().replace(/(\s+\+\s+)|(\s+)/g, '-'); const typeObj = this.attendanceTypes().filter((x) => x.id === id)[0];
if (!typeObj) return '';
return typeObj.name.toLowerCase().replace(/(\s+\+\s+)|(\s+)/g, '-');
} }
show() { show() {
const date = moment(this.form.value.date).format('DD-MMM-YYYY'); const date = moment(this.model().date).format('DD-MMM-YYYY');
this.router.navigate(['/attendance', date]); this.router.navigate(['/attendance', date]);
} }
@@ -116,20 +107,23 @@ export class AttendanceComponent implements OnInit {
this.snackBar.open('', 'Success'); this.snackBar.open('', 'Success');
}, },
error: (error) => { error: (error) => {
this.snackBar.open(error, 'Danger'); this.snackBar.open(error as string, 'Danger');
}, },
}); });
} }
getAttendance(): Attendance { getAttendance(): Attendance {
const formModel = this.form.value; const formModel = this.model();
this.info.date = moment(formModel.date).format('DD-MMM-YYYY'); const currentInfo = this.info();
const array = this.form.controls.attendances; currentInfo.date = moment(formModel.date).format('DD-MMM-YYYY');
const array = formModel.attendances;
if (array) { if (array) {
this.info.body.forEach((item, index) => { currentInfo.body.forEach((item, index) => {
item.attendanceType.id = array.controls[index].value.attendanceType ?? 0; if (array[index]) {
item.attendanceType.id = array[index].attendanceType ?? 0;
}
}); });
} }
return this.info; return currentInfo;
} }
} }
@@ -1,18 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { Attendance } from './attendance';
import { attendanceResolver } from './attendance.resolver';
describe('attendanceResolver', () => {
const executeResolver: ResolveFn<Attendance> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => attendanceResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});
@@ -1,10 +0,0 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { Attendance } from './attendance';
import { AttendanceService } from './attendance.service';
export const attendanceResolver: ResolveFn<Attendance> = (route) => {
const date = route.paramMap.get('date');
return inject(AttendanceService).get(date);
};
@@ -1,9 +1,7 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service'; import { authGuard } from '../auth/auth-guard.service';
import { attendanceTypeResolver } from './attendance-type.resolver';
import { AttendanceComponent } from './attendance.component'; import { AttendanceComponent } from './attendance.component';
import { attendanceResolver } from './attendance.resolver';
export const routes: Routes = [ export const routes: Routes = [
{ {
@@ -13,10 +11,6 @@ export const routes: Routes = [
data: { data: {
permission: 'Attendance', permission: 'Attendance',
}, },
resolve: {
info: attendanceResolver,
attendanceTypes: attendanceTypeResolver,
},
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
{ {
@@ -26,10 +20,6 @@ export const routes: Routes = [
data: { data: {
permission: 'Attendance', permission: 'Attendance',
}, },
resolve: {
info: attendanceResolver,
attendanceTypes: attendanceTypeResolver,
},
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
]; ];
@@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient, httpResource } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, Signal } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@@ -7,23 +7,21 @@ import { ErrorLoggerService } from '../core/error-logger.service';
import { Attendance } from './attendance'; import { Attendance } from './attendance';
const url = '/api/attendance'; const url = '/api/attendance';
const serviceName = 'AttendanceService';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AttendanceService { export class AttendanceService {
private http = inject(HttpClient); private http = inject(HttpClient);
private log = inject(ErrorLoggerService); private log = inject(ErrorLoggerService);
get(date: string | null): Observable<Attendance> { get(date: Signal<string | null>) {
const getUrl: string = date === null ? url : `${url}/${date}`; return httpResource<Attendance>(() => {
return this.http const getUrl: string = date() === null ? url : `${url}/${date()}`;
.get<Attendance>(getUrl) return getUrl;
.pipe(catchError(this.log.handleError(serviceName, `get date=${date}`))) as Observable<Attendance>; });
} }
save(attendance: Attendance): Observable<Attendance> { save(attendance: Attendance): Observable<Attendance> {
return this.http return this.http
.post<Attendance>(`${url}/${attendance.date}`, attendance) .post<Attendance>(`${url}/${attendance.date}`, attendance)
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<Attendance>; .pipe(catchError(this.log.handleError('AttendanceService', 'save'))) as Observable<Attendance>;
} }
} }
@@ -1,16 +1,16 @@
<h2>Login</h2> <h2>Login</h2>
<form [formGroup]="form" class="flex-col"> <form class="flex-col" [formRoot]="form">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Username</mat-label> <mat-label>Username</mat-label>
<input matInput #nameElement formControlName="username" /> <input matInput [formField]="form.username" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Password</mat-label> <mat-label>Password</mat-label>
<input matInput formControlName="password" [type]="hide ? 'password' : 'text'" (keyup.enter)="login()" /> <input matInput [formField]="form.password" [type]="hide ? 'password' : 'text'" (keyup.enter)="login()" />
<mat-icon matSuffix (click)="hide = !hide">{{ hide ? 'visibility' : 'visibility_off' }}</mat-icon> <mat-icon matSuffix (click)="hide = !hide">{{ hide ? 'visibility' : 'visibility_off' }}</mat-icon>
</mat-form-field> </mat-form-field>
</div> </div>
@@ -20,7 +20,7 @@
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Otp</mat-label> <mat-label>Otp</mat-label>
<input matInput formControlName="otp" /> <input matInput [formField]="form.otp" />
</mat-form-field> </mat-form-field>
</div> </div>
} }
+24 -22
View File
@@ -1,5 +1,5 @@
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'; import { ChangeDetectorRef, Component, inject, OnInit, afterNextRender, linkedSignal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider'; import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
@@ -11,13 +11,19 @@ import { ActivatedRoute, Router } from '@angular/router';
import { CookieService } from '../../shared/cookie.service'; import { CookieService } from '../../shared/cookie.service';
import { AuthService } from '../auth.service'; import { AuthService } from '../auth.service';
export interface LoginFormData {
username: string;
password: string;
otp: string;
}
@Component({ @Component({
selector: 'app-login', selector: 'app-login',
templateUrl: './login.component.html', templateUrl: './login.component.html',
styleUrls: ['./login.component.css'], styleUrls: ['./login.component.css'],
imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatIconModule, MatDividerModule, MatButtonModule], imports: [FormField, FormRoot, MatFormFieldModule, MatInputModule, MatIconModule, MatDividerModule, MatButtonModule],
}) })
export class LoginComponent implements OnInit, AfterViewInit { export class LoginComponent implements OnInit {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private auth = inject(AuthService); private auth = inject(AuthService);
private router = inject(Router); private router = inject(Router);
@@ -25,12 +31,16 @@ export class LoginComponent implements OnInit, AfterViewInit {
private cs = inject(CookieService); private cs = inject(CookieService);
private cd = inject(ChangeDetectorRef); private cd = inject(ChangeDetectorRef);
@ViewChild('nameElement', { static: true }) nameElement!: ElementRef<HTMLInputElement>; model = linkedSignal({
form: FormGroup<{ source: () => null,
username: FormControl<string>; computation: (): LoginFormData => ({
password: FormControl<string>; username: '',
otp: FormControl<number | null>; password: '',
}>; otp: '',
}),
});
form = createForm(this.model);
hide: boolean; hide: boolean;
showOtp: boolean; showOtp: boolean;
@@ -40,10 +50,8 @@ export class LoginComponent implements OnInit, AfterViewInit {
constructor() { constructor() {
this.hide = true; this.hide = true;
this.showOtp = false; this.showOtp = false;
this.form = new FormGroup({ afterNextRender(() => {
username: new FormControl('', { validators: Validators.required, nonNullable: true }), this.form().focusBoundControl();
password: new FormControl('', { validators: Validators.required, nonNullable: true }),
otp: new FormControl(0),
}); });
} }
@@ -51,17 +59,11 @@ export class LoginComponent implements OnInit, AfterViewInit {
this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; this.returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/';
} }
ngAfterViewInit() {
setTimeout(() => {
this.nameElement.nativeElement.focus();
}, 0);
}
login(): void { login(): void {
const formModel = this.form.value; const formModel = this.model();
const username = formModel.username ?? ''; const username = formModel.username ?? '';
const password = formModel.password ?? ''; const password = formModel.password ?? '';
const otp = formModel.otp ?? 0; const otp = Number(formModel.otp) || 0;
this.auth this.auth
.login(username, password, otp) .login(username, password, otp)
// .pipe(first()) // .pipe(first())
@@ -1,72 +0,0 @@
import { DataSource } from '@angular/cdk/collections';
import { EventEmitter } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort, Sort } from '@angular/material/sort';
import { merge, Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
import { BalanceSheetItem } from './balance-sheet-item';
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
const compare = (a: string | number, b: string | number, isAsc: boolean) => (a < b ? -1 : 1) * (isAsc ? 1 : -1);
export class BalanceSheetDataSource extends DataSource<BalanceSheetItem> {
constructor(
public data: BalanceSheetItem[],
private paginator?: MatPaginator,
private sort?: MatSort,
) {
super();
}
connect(): Observable<BalanceSheetItem[]> {
const dataMutations: (EventEmitter<PageEvent> | EventEmitter<Sort>)[] = [];
if (this.paginator) {
dataMutations.push((this.paginator as MatPaginator).page);
}
if (this.sort) {
dataMutations.push((this.sort as MatSort).sortChange);
}
// Set the paginators length
if (this.paginator) {
this.paginator.length = this.data.length;
}
return merge(observableOf(this.data), ...dataMutations).pipe(
map(() => this.getPagedData(this.getSortedData([...this.data]))),
);
}
disconnect() {}
private getPagedData(data: BalanceSheetItem[]) {
if (this.paginator === undefined) {
return data;
}
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
return data.splice(startIndex, this.paginator.pageSize);
}
private getSortedData(data: BalanceSheetItem[]) {
if (this.sort === undefined) {
return data;
}
if (!this.sort.active || this.sort.direction === '') {
return data;
}
const sort = this.sort as MatSort;
return data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'name':
if (a.name === undefined || b.name === undefined) {
return 0;
}
return compare(a.name, b.name, isAsc);
default:
return 0;
}
});
}
}
@@ -1,59 +1,65 @@
<h2>Balance Sheet</h2> <h2>Balance Sheet</h2>
<form [formGroup]="form" class="flex-col"> <form class="flex-col" [formRoot]="form">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Date</mat-label> <mat-label>Date</mat-label>
<input matInput #dateElement [matDatepicker]="dateInput" formControlName="date" autocomplete="off" /> <input matInput #dateElement [matDatepicker]="dateInput" [formField]="form.date" autocomplete="off" />
<mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle>
<mat-datepicker #dateInput></mat-datepicker> <mat-datepicker #dateInput></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="show()">Show</button> <button mat-raised-button color="primary" (click)="show()">Show</button>
</div> </div>
</form> </form>
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements"> @if (infoResource.isLoading()) {
<!-- Group Column --> <app-skeleton-loader type="table" [count]="1" />
<ng-container matColumnDef="group"> } @else if (infoResource.error()) {
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell> <app-error-state [message]="'Failed to load data.'" (retryAction)="infoResource.reload()" />
<mat-cell *matCellDef="let row">{{ row.group }}</mat-cell> } @else {
<mat-footer-cell *matFooterCellDef>{{ info.footer?.group }}</mat-footer-cell> <mat-table #table [dataSource]="dataSource()" matSort (matSortChange)="sortData($event)">
</ng-container> <!-- Group Column -->
<ng-container matColumnDef="group">
<mat-header-cell *matHeaderCellDef mat-sort-header>Group</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.group }}</mat-cell>
<mat-footer-cell *matFooterCellDef>{{ info().footer?.group }}</mat-footer-cell>
</ng-container>
<!-- Name Column --> <!-- Name Column -->
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.name }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.name }}</mat-cell>
<mat-footer-cell *matFooterCellDef>{{ info.footer?.name }}</mat-footer-cell> <mat-footer-cell *matFooterCellDef>{{ info().footer?.name }}</mat-footer-cell>
</ng-container> </ng-container>
<!-- SubAmount Column --> <!-- SubAmount Column -->
<ng-container matColumnDef="subAmount"> <ng-container matColumnDef="subAmount">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.subAmount | currency: 'INR' | clear }}</mat-cell> <mat-cell *matCellDef="let row" class="right">{{ row.subAmount | currency: 'INR' | clear }}</mat-cell>
<mat-footer-cell *matFooterCellDef class="right" <mat-footer-cell *matFooterCellDef class="right"
>{{ info.footer?.subAmount | currency: 'INR' | clear }} >{{ $safeNavigationMigration(info().footer?.subAmount) | currency: 'INR' | clear }}
</mat-footer-cell> </mat-footer-cell>
</ng-container> </ng-container>
<!-- Total Column --> <!-- Total Column -->
<ng-container matColumnDef="total"> <ng-container matColumnDef="total">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Total</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header class="right">Total</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' | clear }}</mat-cell> <mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' | clear }}</mat-cell>
<mat-footer-cell *matFooterCellDef class="right" <mat-footer-cell *matFooterCellDef class="right"
>{{ info.footer?.amount | currency: 'INR' | clear }} >{{ $safeNavigationMigration(info().footer?.amount) | currency: 'INR' | clear }}
</mat-footer-cell> </mat-footer-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row> <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
<mat-footer-row *matFooterRowDef="displayedColumns"></mat-footer-row> <mat-footer-row *matFooterRowDef="displayedColumns"></mat-footer-row>
</mat-table> </mat-table>
<mat-paginator <mat-paginator
#paginator (page)="handlePageEvent($event)"
[length]="dataSource.data.length" [length]="(this.info().body ?? []).length"
[pageIndex]="0" [pageIndex]="pageIndex()"
[pageSize]="50" [pageSize]="pageSize()"
[pageSizeOptions]="[25, 50, 100, 250, 300, 5000]" [pageSizeOptions]="[25, 50, 100, 250, 300, 5000]"
> >
</mat-paginator> </mat-paginator>
}
@@ -1,26 +1,32 @@
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from '@angular/common';
import { Component, ElementRef, HostListener, inject, OnInit, ViewChild } from '@angular/core'; import { Component, HostListener, inject, input, computed, linkedSignal, signal, afterNextRender } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
export interface BalanceSheetFormData {
date: moment.Moment;
}
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { Router } from '@angular/router';
import moment from 'moment'; import moment from 'moment';
import { ClearPipe } from '../shared/clear.pipe'; import { ClearPipe } from '../shared/clear.pipe';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { BalanceSheet } from './balance-sheet'; import { BalanceSheet } from './balance-sheet';
import { BalanceSheetDataSource } from './balance-sheet-datasource'; import { BalanceSheetService } from './balance-sheet.service';
@Component({ @Component({
selector: 'app-balance-sheet', selector: 'app-balance-sheet',
templateUrl: './balance-sheet.component.html', templateUrl: './balance-sheet.component.html',
styleUrls: ['./balance-sheet.component.css'], styleUrls: ['./balance-sheet.component.css'],
imports: [ imports: [
ReactiveFormsModule, FormField,
FormRoot,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatDatepickerModule, MatDatepickerModule,
@@ -30,20 +36,69 @@ import { BalanceSheetDataSource } from './balance-sheet-datasource';
MatPaginatorModule, MatPaginatorModule,
CurrencyPipe, CurrencyPipe,
ClearPipe, ClearPipe,
SkeletonLoaderComponent,
ErrorStateComponent,
], ],
}) })
export class BalanceSheetComponent implements OnInit { export class BalanceSheetComponent {
private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private ser = inject(BalanceSheetService);
pageSize = signal(50);
pageIndex = signal(0);
date = input(null, { transform: (v: string | null | undefined) => v ?? null });
infoResource = this.ser.list(this.date);
info = computed(() => this.infoResource.value() ?? new BalanceSheet());
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator; sortActive = signal('');
@ViewChild(MatSort, { static: true }) sort!: MatSort; sortDirection = signal('');
@ViewChild('dateElement', { static: true }) dateElement!: ElementRef<HTMLInputElement>;
info: BalanceSheet = new BalanceSheet(); formModel = linkedSignal<import('./balance-sheet').BalanceSheet, BalanceSheetFormData>({
dataSource: BalanceSheetDataSource = new BalanceSheetDataSource(this.info.body, this.paginator, this.sort); source: this.info,
form: FormGroup<{ computation: (info: BalanceSheet): BalanceSheetFormData => ({
date: FormControl<moment.Moment>; date: info.date ? moment(info.date, 'DD-MMM-YYYY') : moment(new Date()),
}>; }),
});
form = createForm(this.formModel);
sortedList = computed(() => {
const data = this.info().body ?? [];
const active = this.sortActive();
const direction = this.sortDirection();
if (!active || direction === '') {
return data;
}
return [...data].sort((a, b) => {
const isAsc = direction === 'asc';
switch (active) {
case 'group':
return compare(a.group ?? '', b.group ?? '', isAsc);
case 'name':
return compare(a.name ?? '', b.name ?? '', isAsc);
case 'subAmount':
return compare(a.subAmount ?? 0, b.subAmount ?? 0, isAsc);
case 'total':
return compare(a.amount ?? 0, b.amount ?? 0, isAsc);
default:
return 0;
}
});
});
dataSource = computed(() => {
const data = this.sortedList();
const pageIndex = this.pageIndex();
const pageSize = this.pageSize();
return data.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
});
constructor() {
afterNextRender(() => {
this.form().focusBoundControl();
});
}
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['group', 'name', 'subAmount', 'total']; displayedColumns = ['group', 'name', 'subAmount', 'total'];
@@ -51,26 +106,7 @@ export class BalanceSheetComponent implements OnInit {
@HostListener('window:keydown.f2', ['$event']) @HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.dateElement.nativeElement.focus(); this.form().focusBoundControl();
this.dateElement.nativeElement.select();
}
constructor() {
this.form = new FormGroup({
date: new FormControl(moment(new Date()), { nonNullable: true }),
});
}
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { info: BalanceSheet };
this.info = data.info;
this.form.setValue({
date: moment(this.info.date, 'DD-MMM-YYYY'),
});
this.dataSource = new BalanceSheetDataSource(this.info.body, this.paginator, this.sort);
});
} }
show() { show() {
@@ -79,7 +115,19 @@ export class BalanceSheetComponent implements OnInit {
} }
getDate(): string { getDate(): string {
const formModel = this.form.value; const formModel = this.formModel();
return moment(formModel.date).format('DD-MMM-YYYY'); return moment(formModel.date).format('DD-MMM-YYYY');
} }
handlePageEvent(e: PageEvent) {
this.pageSize.set(e.pageSize);
this.pageIndex.set(e.pageIndex);
}
sortData(sort: Sort) {
this.sortActive.set(sort.active);
this.sortDirection.set(sort.direction);
}
} }
const compare = (a: string | number | boolean, b: string | number | boolean, isAsc: boolean) =>
(a < b ? -1 : 1) * (isAsc ? 1 : -1);
@@ -1,18 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { BalanceSheet } from './balance-sheet';
import { balanceSheetResolver } from './balance-sheet.resolver';
describe('balanceSheetResolver', () => {
const executeResolver: ResolveFn<BalanceSheet> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => balanceSheetResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});
@@ -1,10 +0,0 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { BalanceSheet } from './balance-sheet';
import { BalanceSheetService } from './balance-sheet.service';
export const balanceSheetResolver: ResolveFn<BalanceSheet> = (route) => {
const date = route.paramMap.get('date');
return inject(BalanceSheetService).list(date);
};
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service'; import { authGuard } from '../auth/auth-guard.service';
import { BalanceSheetComponent } from './balance-sheet.component'; import { BalanceSheetComponent } from './balance-sheet.component';
import { balanceSheetResolver } from './balance-sheet.resolver';
export const routes: Routes = [ export const routes: Routes = [
{ {
@@ -12,9 +11,6 @@ export const routes: Routes = [
data: { data: {
permission: 'Balance Sheet', permission: 'Balance Sheet',
}, },
resolve: {
info: balanceSheetResolver,
},
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
{ {
@@ -24,9 +20,6 @@ export const routes: Routes = [
data: { data: {
permission: 'Balance Sheet', permission: 'Balance Sheet',
}, },
resolve: {
info: balanceSheetResolver,
},
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
]; ];
@@ -1,25 +1,17 @@
import { HttpClient } from '@angular/common/http'; import { httpResource } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { Injectable, Signal } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from '../core/error-logger.service';
import { BalanceSheet } from './balance-sheet'; import { BalanceSheet } from './balance-sheet';
const url = '/api/balance-sheet'; const url = '/api/balance-sheet';
const serviceName = 'BalanceSheetService';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class BalanceSheetService { export class BalanceSheetService {
private http = inject(HttpClient); list(date: Signal<string | null>) {
private log = inject(ErrorLoggerService); return httpResource<BalanceSheet>(() => {
const listUrl = date() === null ? url : `${url}/${date()}`;
list(date: string | null): Observable<BalanceSheet> { return listUrl;
const listUrl = date === null ? url : `${url}/${date}`; });
return this.http
.get<BalanceSheet>(listUrl)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<BalanceSheet>;
} }
} }
@@ -1,16 +0,0 @@
import { DataSource } from '@angular/cdk/collections';
import { Observable, of as observableOf } from 'rxjs';
import { BatchIntegrity } from './batch-integrity';
export class BatchIntegrityReportDatasource extends DataSource<BatchIntegrity> {
constructor(public data: BatchIntegrity[]) {
super();
}
connect(): Observable<BatchIntegrity[]> {
return observableOf(this.data);
}
disconnect() {}
}
@@ -1,30 +1,36 @@
<h2>Batch Integrity</h2> <h2>Batch Integrity</h2>
<mat-table [dataSource]="dataSource" aria-label="Elements"> @if (infoResource.isLoading()) {
<!-- Batch Column --> <app-skeleton-loader type="table" [count]="1" />
<ng-container matColumnDef="batch"> } @else if (infoResource.error()) {
<mat-header-cell *matHeaderCellDef>Batch</mat-header-cell> <app-error-state [message]="'Failed to load data.'" (retryAction)="infoResource.reload()" />
<mat-cell *matCellDef="let row"> } @else {
{{ row.product }} dated {{ row.date }} &#64; {{ row.price | currency: 'INR' }} is showing <mat-table [dataSource]="info()">
{{ row.showing }} instead of {{ row.actual }} <!-- Batch Column -->
</mat-cell> <ng-container matColumnDef="batch">
</ng-container> <mat-header-cell *matHeaderCellDef>Batch</mat-header-cell>
<mat-cell *matCellDef="let row">
{{ row.product }} dated {{ row.date }} &#64; {{ row.price | currency: 'INR' }} is showing
{{ row.showing }} instead of {{ row.actual }}
</mat-cell>
</ng-container>
<!-- Details Column --> <!-- Details Column -->
<ng-container matColumnDef="details"> <ng-container matColumnDef="details">
<mat-header-cell *matHeaderCellDef>Details</mat-header-cell> <mat-header-cell *matHeaderCellDef>Details</mat-header-cell>
<mat-cell *matCellDef="let row"> <mat-cell *matCellDef="let row">
<ul> <ul>
@for (detail of row.details; track detail) { @for (detail of row.details; track detail) {
<li> <li>
<a [routerLink]="detail.url">{{ detail.type }}</a> on {{ detail.date }} of {{ detail.quantity }} &#64; <a [routerLink]="detail.url">{{ detail.type }}</a> on {{ detail.date }} of {{ detail.quantity }} &#64;
{{ detail.price | currency: 'INR' }} {{ detail.price | currency: 'INR' }}
</li> </li>
} }
</ul> </ul>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row> <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table> </mat-table>
}
@@ -1,32 +1,27 @@
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject, computed } from '@angular/core';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { BatchIntegrity } from './batch-integrity'; import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { BatchIntegrityReportDatasource } from './batch-integrity-report-datasource'; import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { BatchIntegrityReportService } from './batch-integrity-report.service';
@Component({ @Component({
selector: 'app-batch-integrity', selector: 'app-batch-integrity',
templateUrl: './batch-integrity-report.component.html', templateUrl: './batch-integrity-report.component.html',
styleUrls: ['./batch-integrity-report.component.css'], styleUrls: ['./batch-integrity-report.component.css'],
imports: [MatTableModule, RouterModule, CurrencyPipe], imports: [MatTableModule, RouterModule, CurrencyPipe, SkeletonLoaderComponent, ErrorStateComponent],
}) })
export class BatchIntegrityReportComponent implements OnInit { export class BatchIntegrityReportComponent {
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private ser = inject(BatchIntegrityReportService);
info: BatchIntegrity[] = [];
dataSource: BatchIntegrityReportDatasource = new BatchIntegrityReportDatasource(this.info);
displayedColumns = ['batch', 'details']; displayedColumns = ['batch', 'details'];
ngOnInit() { infoResource = this.ser.batchIntegrity();
this.route.data.subscribe((value) => { info = computed(() => this.infoResource.value() ?? []);
const data = value as { info: BatchIntegrity[] };
this.info = data.info;
this.dataSource = new BatchIntegrityReportDatasource(this.info);
});
}
refresh() { refresh() {
this.router.navigate([]); this.router.navigate([]);
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service'; import { authGuard } from '../auth/auth-guard.service';
import { BatchIntegrityReportComponent } from './batch-integrity-report.component'; import { BatchIntegrityReportComponent } from './batch-integrity-report.component';
import { batchIntegrityResolver } from './batch-integrity.resolver';
export const routes: Routes = [ export const routes: Routes = [
{ {
@@ -12,9 +11,6 @@ export const routes: Routes = [
data: { data: {
permission: 'Product Ledger', permission: 'Product Ledger',
}, },
resolve: {
info: batchIntegrityResolver,
},
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
]; ];
@@ -1,24 +1,18 @@
import { HttpClient } from '@angular/common/http'; import { httpResource } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from '../core/error-logger.service';
import { BatchIntegrity } from './batch-integrity'; import { BatchIntegrity } from './batch-integrity';
const serviceName = 'BatchIntegrityReportService';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class BatchIntegrityReportService { export class BatchIntegrityReportService {
private http = inject(HttpClient); batchIntegrity() {
private log = inject(ErrorLoggerService);
batchIntegrity(): Observable<BatchIntegrity[]> {
const url = '/api/batch-integrity'; const url = '/api/batch-integrity';
return this.http return httpResource<BatchIntegrity[]>(() => ({
.post<BatchIntegrity[]>(url, {}) url,
.pipe(catchError(this.log.handleError(serviceName, 'batchIntegrity'))) as Observable<BatchIntegrity[]>; method: 'POST',
body: {},
}));
} }
} }
@@ -1,18 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { BatchIntegrity } from './batch-integrity';
import { batchIntegrityResolver } from './batch-integrity.resolver';
describe('batchIntegrityResolver', () => {
const executeResolver: ResolveFn<BatchIntegrity[]> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => batchIntegrityResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});
@@ -1,9 +0,0 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { BatchIntegrity } from './batch-integrity';
import { BatchIntegrityReportService } from './batch-integrity-report.service';
export const batchIntegrityResolver: ResolveFn<BatchIntegrity[]> = () => {
return inject(BatchIntegrityReportService).batchIntegrity();
};
@@ -1,16 +0,0 @@
import { DataSource } from '@angular/cdk/collections';
import { Observable, of as observableOf } from 'rxjs';
import { CashFlowItem } from './cash-flow-item';
export class CashFlowDataSource extends DataSource<CashFlowItem> {
constructor(private data: CashFlowItem[]) {
super();
}
connect(): Observable<CashFlowItem[]> {
return observableOf(this.data);
}
disconnect() {}
}
@@ -1,39 +1,45 @@
<h2>Cash Flow</h2> <h2>Cash Flow</h2>
<form [formGroup]="form" class="flex-col"> <form class="flex-col" [formRoot]="form">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Start Date</mat-label> <mat-label>Start Date</mat-label>
<input matInput #startDateElement [matDatepicker]="startDate" formControlName="startDate" autocomplete="off" /> <input matInput [matDatepicker]="startDatePicker" [formField]="form.startDate" autocomplete="off" />
<mat-datepicker-toggle matSuffix [for]="startDate"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
<mat-datepicker #startDate></mat-datepicker> <mat-datepicker #startDatePicker></mat-datepicker>
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Finish Date</mat-label> <mat-label>Finish Date</mat-label>
<input matInput [matDatepicker]="finishDate" formControlName="finishDate" autocomplete="off" /> <input matInput [matDatepicker]="finishDate" [formField]="form.finishDate" autocomplete="off" />
<mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="finishDate"></mat-datepicker-toggle>
<mat-datepicker #finishDate></mat-datepicker> <mat-datepicker #finishDate></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button> <button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button>
</div> </div>
</form> </form>
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements"> @if (infoResource.isLoading()) {
<!-- Name Column --> <app-skeleton-loader type="table" [count]="1" />
<ng-container matColumnDef="name"> } @else if (infoResource.error()) {
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell> <app-error-state [message]="'Failed to load cash flow.'" (retryAction)="infoResource.reload()" />
<mat-cell *matCellDef="let row" } @else {
><a [routerLink]="row.url" [queryParams]="{ startDate: info.startDate, finishDate: info.finishDate }" <mat-table #table [dataSource]="dataSource()" matSort (matSortChange)="sortData($event)">
>{{ row.name }} <!-- Name Column -->
</a></mat-cell <ng-container matColumnDef="name">
> <mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
</ng-container> <mat-cell *matCellDef="let row"
><a [routerLink]="row.url" [queryParams]="{ startDate: info().startDate, finishDate: info().finishDate }"
>{{ row.name }}
</a></mat-cell
>
</ng-container>
<!-- Amount Column --> <!-- Amount Column -->
<ng-container matColumnDef="amount"> <ng-container matColumnDef="amount">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header class="right">Amount</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' | clear }}</mat-cell> <mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' | clear }}</mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row> <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table> </mat-table>
}
@@ -1,25 +1,32 @@
import { CurrencyPipe } from '@angular/common'; import { CurrencyPipe } from '@angular/common';
import { Component, ElementRef, HostListener, inject, OnInit, ViewChild } from '@angular/core'; import { Component, HostListener, inject, input, computed, linkedSignal, afterNextRender, signal } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
export interface CashFlowFormData {
startDate: moment.Moment;
finishDate: moment.Moment;
}
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { Router, RouterModule } from '@angular/router';
import moment from 'moment'; import moment from 'moment';
import { ClearPipe } from '../shared/clear.pipe'; import { ClearPipe } from '../shared/clear.pipe';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { CashFlow } from './cash-flow'; import { CashFlow } from './cash-flow';
import { CashFlowDataSource } from './cash-flow-datasource'; import { CashFlowService } from './cash-flow.service';
@Component({ @Component({
selector: 'app-cash-flow', selector: 'app-cash-flow',
templateUrl: './cash-flow.component.html', templateUrl: './cash-flow.component.html',
styleUrls: ['./cash-flow.component.css'], styleUrls: ['./cash-flow.component.css'],
imports: [ imports: [
ReactiveFormsModule, FormField,
FormRoot,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatDatepickerModule, MatDatepickerModule,
@@ -29,19 +36,54 @@ import { CashFlowDataSource } from './cash-flow-datasource';
RouterModule, RouterModule,
CurrencyPipe, CurrencyPipe,
ClearPipe, ClearPipe,
SkeletonLoaderComponent,
ErrorStateComponent,
], ],
}) })
export class CashFlowComponent implements OnInit { export class CashFlowComponent {
private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private ser = inject(CashFlowService);
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
startDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
finishDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
sortActive = signal('');
sortDirection = signal('');
@ViewChild('startDateElement', { static: true }) startDate!: ElementRef<HTMLInputElement>; infoResource = this.ser.list(this.id, this.startDate, this.finishDate);
info: CashFlow = new CashFlow(); info = computed(() => this.infoResource.value() ?? new CashFlow());
dataSource: CashFlowDataSource = new CashFlowDataSource(CashFlow.Data(this.info)); sortedList = computed(() => {
form: FormGroup<{ const data = CashFlow.Data(this.info());
startDate: FormControl<moment.Moment>; const active = this.sortActive();
finishDate: FormControl<moment.Moment>; const direction = this.sortDirection();
}>;
if (!active || direction === '') {
return data;
}
return [...data].sort((a, b) => {
const isAsc = direction === 'asc';
switch (active) {
case 'name':
return compare(a.name ?? '', b.name ?? '', isAsc);
case 'amount':
return compare(a.amount ?? 0, b.amount ?? 0, isAsc);
default:
return 0;
}
});
});
dataSource = computed(() => this.sortedList());
model = linkedSignal({
source: this.info,
computation: (info: CashFlow): CashFlowFormData => ({
startDate: info.startDate ? moment(info.startDate, 'DD-MMM-YYYY') : moment(new Date()),
finishDate: info.finishDate ? moment(info.finishDate, 'DD-MMM-YYYY') : moment(new Date()),
}),
});
form = createForm(this.model);
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['name', 'amount']; displayedColumns = ['name', 'amount'];
@@ -49,27 +91,13 @@ export class CashFlowComponent implements OnInit {
@HostListener('window:keydown.f2', ['$event']) @HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.startDate.nativeElement.focus(); // TODO: also select the contents
this.startDate.nativeElement.select(); this.form.startDate().focusBoundControl();
} }
constructor() { constructor() {
this.form = new FormGroup({ afterNextRender(() => {
startDate: new FormControl(moment(new Date()), { nonNullable: true }), this.form.startDate().focusBoundControl();
finishDate: new FormControl(moment(new Date()), { nonNullable: true }),
});
}
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { info: CashFlow };
this.info = data.info;
this.form.setValue({
startDate: moment(this.info.startDate, 'DD-MMM-YYYY'),
finishDate: moment(this.info.finishDate, 'DD-MMM-YYYY'),
});
this.dataSource = new CashFlowDataSource(CashFlow.Data(this.info));
}); });
} }
@@ -84,11 +112,18 @@ export class CashFlowComponent implements OnInit {
} }
getInfo(): CashFlow { getInfo(): CashFlow {
const formModel = this.form.value; const formModel = this.model();
return new CashFlow({ return new CashFlow({
startDate: moment(formModel.startDate).format('DD-MMM-YYYY'), startDate: moment(formModel.startDate).format('DD-MMM-YYYY'),
finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'), finishDate: moment(formModel.finishDate).format('DD-MMM-YYYY'),
}); });
} }
sortData(sort: Sort) {
this.sortActive.set(sort.active);
this.sortDirection.set(sort.direction);
}
} }
const compare = (a: string | number | boolean, b: string | number | boolean, isAsc: boolean) =>
(a < b ? -1 : 1) * (isAsc ? 1 : -1);
@@ -1,18 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { CashFlow } from './cash-flow';
import { cashFlowResolver } from './cash-flow.resolver';
describe('cashFlowResolver', () => {
const executeResolver: ResolveFn<CashFlow> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => cashFlowResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});
@@ -1,12 +0,0 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { CashFlow } from './cash-flow';
import { CashFlowService } from './cash-flow.service';
export const cashFlowResolver: ResolveFn<CashFlow> = (route) => {
const id = route.paramMap.get('id');
const startDate = route.queryParamMap.get('startDate') || null;
const finishDate = route.queryParamMap.get('finishDate') || null;
return inject(CashFlowService).list(id, startDate, finishDate);
};
@@ -2,7 +2,6 @@ import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service'; import { authGuard } from '../auth/auth-guard.service';
import { CashFlowComponent } from './cash-flow.component'; import { CashFlowComponent } from './cash-flow.component';
import { cashFlowResolver } from './cash-flow.resolver';
export const routes: Routes = [ export const routes: Routes = [
{ {
@@ -12,9 +11,6 @@ export const routes: Routes = [
data: { data: {
permission: 'Cash Flow', permission: 'Cash Flow',
}, },
resolve: {
info: cashFlowResolver,
},
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
{ {
@@ -24,9 +20,6 @@ export const routes: Routes = [
data: { data: {
permission: 'Cash Flow', permission: 'Cash Flow',
}, },
resolve: {
info: cashFlowResolver,
},
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
]; ];
+19 -29
View File
@@ -1,39 +1,29 @@
import { HttpClient, HttpParams } from '@angular/common/http'; import { httpResource } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { Injectable, Signal } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from '../core/error-logger.service';
import { CashFlow } from './cash-flow'; import { CashFlow } from './cash-flow';
const url = '/api/cash-flow'; const url = '/api/cash-flow';
const serviceName = 'CashFlowService';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class CashFlowService { export class CashFlowService {
private http = inject(HttpClient); list(id: Signal<string | null>, startDate: Signal<string | null>, finishDate: Signal<string | null>) {
private log = inject(ErrorLoggerService); return httpResource<CashFlow>(() => {
const params: Record<string, string> = {};
list(id: string | null, startDate: string | null, finishDate: string | null): Observable<CashFlow> { const startDate_val = startDate();
const options = { params: new HttpParams() }; if (startDate_val !== null) params['s'] = startDate_val;
if (startDate !== null) { const finishDate_val = finishDate();
options.params = options.params.set('s', startDate); if (finishDate_val !== null) params['f'] = finishDate_val;
} let resultUrl: string;
if (finishDate !== null) { if (id() !== null) {
options.params = options.params.set('f', finishDate); resultUrl = `${url}/${id()}`;
} } else if (startDate() || finishDate()) {
let listUrl = ''; resultUrl = `${url}/data`;
if (id !== null) { } else {
listUrl = `${url}/${id}`; resultUrl = url;
} else if (startDate || finishDate) { }
listUrl = `${url}/data`; return { url: resultUrl, params };
} else { });
listUrl = url;
}
return this.http
.get<CashFlow>(listUrl, options)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<CashFlow>;
} }
} }
@@ -1,30 +1,37 @@
<h2>Client</h2> <h2>Client</h2>
<form [formGroup]="form" class="flex-col wrapped"> @if (itemResource.isLoading()) {
<div class="row-container"> <app-skeleton-loader type="card" [count]="1" />
<mat-form-field class="flex-auto"> <app-skeleton-loader type="line" [count]="10" />
<mat-label>Code</mat-label> } @else if (itemResource.error()) {
<input matInput formControlName="code" /> <app-error-state [message]="'Failed to load data.'" (retryAction)="itemResource.reload()" />
</mat-form-field> } @else {
</div> <form class="flex-col wrapped" [formRoot]="form">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Name</mat-label> <mat-label>Code</mat-label>
<input matInput #nameElement formControlName="name" /> <input matInput [formField]="form.code" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="row-container"> <div class="row-container">
<mat-checkbox formControlName="enabled" class="flex-auto">Is Enabled?</mat-checkbox> <mat-form-field class="flex-auto">
</div> <mat-label>Name</mat-label>
<div class="row-container"> <input matInput [formField]="form.name" />
<mat-form-field class="flex-auto"> </mat-form-field>
<mat-label>OTP</mat-label> </div>
<input matInput formControlName="otp" /> <div class="row-container">
</mat-form-field> <mat-checkbox [formField]="form.enabled" class="flex-auto">Is Enabled?</mat-checkbox>
</div> </div>
</form> <div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>OTP</mat-label>
<input matInput [formField]="form.otp" />
</mat-form-field>
</div>
</form>
<div class="row-container"> <div class="row-container">
<button mat-raised-button color="primary" (click)="save()">Save</button> <button mat-raised-button color="primary" (click)="save()">Save</button>
<button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button> <button mat-raised-button color="warn" (click)="confirmDelete()">Delete</button>
</div> </div>
}
@@ -1,73 +1,71 @@
import { AfterViewInit, Component, ElementRef, inject, OnInit, ViewChild } from '@angular/core'; import { Component, inject, afterNextRender, linkedSignal, input, computed } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router'; import { Router } from '@angular/router';
import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from '../../shared/confirm-dialog/confirm-dialog.component';
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
import { Client } from '../client'; import { Client } from '../client';
import { ClientService } from '../client.service'; import { ClientService } from '../client.service';
export interface ClientFormData {
code: string;
name: string;
enabled: boolean;
otp: string;
}
@Component({ @Component({
selector: 'app-client-detail', selector: 'app-client-detail',
templateUrl: './client-detail.component.html', templateUrl: './client-detail.component.html',
styleUrls: ['./client-detail.component.css'], styleUrls: ['./client-detail.component.css'],
imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule, MatCheckboxModule, MatButtonModule], imports: [
FormField,
FormRoot,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
SkeletonLoaderComponent,
ErrorStateComponent,
],
}) })
export class ClientDetailComponent implements OnInit, AfterViewInit { export class ClientDetailComponent {
private route = inject(ActivatedRoute); id = input(null, { transform: (v: string | null | undefined) => v ?? null });
private router = inject(Router); private router = inject(Router);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
private ser = inject(ClientService); private ser = inject(ClientService);
@ViewChild('nameElement', { static: true }) nameElement!: ElementRef<HTMLInputElement>; itemResource = this.ser.get(this.id);
form: FormGroup<{
code: FormControl<number | string>;
name: FormControl<string | null>;
enabled: FormControl<boolean>;
otp: FormControl<number | null>;
}>;
item: Client = new Client(); item = computed(() => this.itemResource.value() ?? new Client());
model = linkedSignal({
source: this.item,
computation: (item): ClientFormData => ({
code: item.code ? String(item.code) : '(Auto)',
name: item.name ?? '',
enabled: item.enabled ?? false,
otp: item.otp ? String(item.otp) : '',
}),
});
form = createForm(this.model);
constructor() { constructor() {
this.form = new FormGroup({ afterNextRender(() => {
code: new FormControl<number | string>({ value: '(Auto)', disabled: true }, { nonNullable: true }), this.form.name().focusBoundControl();
name: new FormControl<string | null>(null),
enabled: new FormControl<boolean>(false, { nonNullable: true }),
otp: new FormControl<number | null>(null),
}); });
} }
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { item: Client };
this.showItem(data.item);
});
}
showItem(item: Client) {
this.item = item;
this.form.setValue({
code: this.item.code || '(Auto)',
name: this.item.name,
enabled: this.item.enabled,
otp: this.item.otp || null,
});
}
ngAfterViewInit() {
setTimeout(() => {
this.nameElement.nativeElement.focus();
}, 0);
}
save() { save() {
this.ser.update(this.getItem()).subscribe({ this.ser.update(this.getItem()).subscribe({
next: () => { next: () => {
@@ -75,19 +73,19 @@ export class ClientDetailComponent implements OnInit, AfterViewInit {
this.router.navigateByUrl('/clients'); this.router.navigateByUrl('/clients');
}, },
error: (error) => { error: (error) => {
this.snackBar.open(error, 'Danger'); this.snackBar.open(error as string, 'Danger');
}, },
}); });
} }
delete() { delete() {
this.ser.delete(this.item.id).subscribe({ this.ser.delete(this.item().id).subscribe({
next: () => { next: () => {
this.snackBar.open('', 'Success'); this.snackBar.open('', 'Success');
this.router.navigateByUrl('/clients'); this.router.navigateByUrl('/clients');
}, },
error: (error) => { error: (error) => {
this.snackBar.open(error, 'Danger'); this.snackBar.open(error as string, 'Danger');
}, },
}); });
} }
@@ -106,10 +104,11 @@ export class ClientDetailComponent implements OnInit, AfterViewInit {
} }
getItem(): Client { getItem(): Client {
const formModel = this.form.value; const formModel = this.model();
this.item.name = formModel.name ?? ''; const item = this.item();
this.item.enabled = formModel.enabled ?? false; item.name = formModel.name ?? '';
this.item.otp = formModel.otp || undefined; item.enabled = formModel.enabled ?? false;
return this.item; item.otp = formModel.otp ? Number(formModel.otp) : undefined;
return item;
} }
} }
@@ -1,18 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { Client } from './client';
import { clientListResolver } from './client-list.resolver';
describe('clientListResolver', () => {
const executeResolver: ResolveFn<Client[]> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => clientListResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});
@@ -1,9 +0,0 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { Client } from './client';
import { ClientService } from './client.service';
export const clientListResolver: ResolveFn<Client[]> = () => {
return inject(ClientService).list();
};
@@ -1,71 +0,0 @@
import { DataSource } from '@angular/cdk/collections';
import { EventEmitter } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort, Sort } from '@angular/material/sort';
import { merge, Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
import { Client } from '../client';
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
const compare = (a: string | number, b: string | number, isAsc: boolean) => (a < b ? -1 : 1) * (isAsc ? 1 : -1);
export class ClientListDataSource extends DataSource<Client> {
constructor(
public data: Client[],
private paginator?: MatPaginator,
private sort?: MatSort,
) {
super();
}
connect(): Observable<Client[]> {
const dataMutations: (EventEmitter<PageEvent> | EventEmitter<Sort>)[] = [];
if (this.paginator) {
dataMutations.push((this.paginator as MatPaginator).page);
}
if (this.sort) {
dataMutations.push((this.sort as MatSort).sortChange);
}
// Set the paginators length
if (this.paginator) {
this.paginator.length = this.data.length;
}
return merge(observableOf(this.data), ...dataMutations).pipe(
map(() => this.getPagedData(this.getSortedData([...this.data]))),
);
}
disconnect() {}
private getPagedData(data: Client[]) {
if (this.paginator === undefined) {
return data;
}
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
return data.splice(startIndex, this.paginator.pageSize);
}
private getSortedData(data: Client[]) {
if (this.sort === undefined) {
return data;
}
if (!this.sort.active || this.sort.direction === '') {
return data;
}
const sort = this.sort as MatSort;
return data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'name':
return compare(a.name, b.name, isAsc);
case 'id':
return compare(+a.id, +b.id, isAsc);
default:
return 0;
}
});
}
}
@@ -1,55 +1,61 @@
<h2>Clients</h2> <h2>Clients</h2>
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements"> @if (listResource.isLoading()) {
<!-- Code Column --> <app-skeleton-loader type="table" [count]="1" />
<ng-container matColumnDef="code"> } @else if (listResource.error()) {
<mat-header-cell *matHeaderCellDef mat-sort-header>Code</mat-header-cell> <app-error-state [message]="'Failed to load data.'" (retryAction)="listResource.reload()" />
<mat-cell *matCellDef="let row">{{ row.code }}</mat-cell> } @else {
</ng-container> <mat-table #table [dataSource]="dataSource()" matSort (matSortChange)="sortData($event)">
<!-- Code Column -->
<ng-container matColumnDef="code">
<mat-header-cell *matHeaderCellDef mat-sort-header>Code</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.code }}</mat-cell>
</ng-container>
<!-- Name Column --> <!-- Name Column -->
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Name</mat-header-cell>
<mat-cell *matCellDef="let row" <mat-cell *matCellDef="let row"
><a [routerLink]="['/clients', row.id]">{{ row.name }}</a></mat-cell ><a [routerLink]="['/clients', row.id]">{{ row.name }}</a></mat-cell
> >
</ng-container> </ng-container>
<!-- Enabled Column --> <!-- Enabled Column -->
<ng-container matColumnDef="enabled"> <ng-container matColumnDef="enabled">
<mat-header-cell *matHeaderCellDef mat-sort-header>Is Enabled?</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Is Enabled?</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.enabled }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.enabled }}</mat-cell>
</ng-container> </ng-container>
<!-- OTP Column --> <!-- OTP Column -->
<ng-container matColumnDef="otp"> <ng-container matColumnDef="otp">
<mat-header-cell *matHeaderCellDef mat-sort-header>otp</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>otp</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.otp }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.otp }}</mat-cell>
</ng-container> </ng-container>
<!-- Created Column --> <!-- Created Column -->
<ng-container matColumnDef="created"> <ng-container matColumnDef="created">
<mat-header-cell *matHeaderCellDef mat-sort-header>Created</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Created</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.creationDate | localTime }}</mat-cell> <mat-cell *matCellDef="let row">{{ row.creationDate | localTime }}</mat-cell>
</ng-container> </ng-container>
<!-- Last Login Column --> <!-- Last Login Column -->
<ng-container matColumnDef="last"> <ng-container matColumnDef="last">
<mat-header-cell *matHeaderCellDef mat-sort-header>Last Login</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header>Last Login</mat-header-cell>
<mat-cell *matCellDef="let row" <mat-cell *matCellDef="let row"
>{{ row.lastUser }} &#64; {{ row.lastDate ? (row.lastDate | localTime) : 'Never' }}</mat-cell >{{ row.lastUser }} &#64; {{ row.lastDate ? (row.lastDate | localTime) : 'Never' }}</mat-cell
> >
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row> <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table> </mat-table>
<mat-paginator <mat-paginator
#paginator (page)="handlePageEvent($event)"
[length]="dataSource.data.length" [length]="list().length"
[pageIndex]="0" [pageIndex]="pageIndex()"
[pageSize]="50" [pageSize]="pageSize()"
[pageSizeOptions]="[25, 50, 100, 250]" [pageSizeOptions]="[25, 50, 100, 250]"
> >
</mat-paginator> </mat-paginator>
}
@@ -1,35 +1,81 @@
import { Component, inject, OnInit, ViewChild } from '@angular/core'; import { Component, computed, inject, signal } from '@angular/core';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { ErrorStateComponent } from '../../shared/error-state/error-state.component';
import { LocalTimePipe } from '../../shared/local-time.pipe'; import { LocalTimePipe } from '../../shared/local-time.pipe';
import { Client } from '../client'; import { SkeletonLoaderComponent } from '../../shared/skeleton-loader/skeleton-loader.component';
import { ClientListDataSource } from './client-list-datasource'; import { ClientService } from '../client.service';
@Component({ @Component({
selector: 'app-client-list', selector: 'app-client-list',
templateUrl: './client-list.component.html', templateUrl: './client-list.component.html',
styleUrls: ['./client-list.component.css'], styleUrls: ['./client-list.component.css'],
imports: [MatTableModule, MatSortModule, RouterModule, MatPaginatorModule, LocalTimePipe], imports: [
MatTableModule,
MatSortModule,
RouterModule,
MatPaginatorModule,
LocalTimePipe,
SkeletonLoaderComponent,
ErrorStateComponent,
],
}) })
export class ClientListComponent implements OnInit { export class ClientListComponent {
private route = inject(ActivatedRoute); private ser = inject(ClientService);
pageSize = signal(50);
pageIndex = signal(0);
sortActive = signal('');
sortDirection = signal('');
listResource = this.ser.list();
list = computed(() => this.listResource.value() ?? []);
sortedList = computed(() => {
const data = this.list();
const active = this.sortActive();
const direction = this.sortDirection();
if (!active || direction === '') {
return data;
}
return [...data].sort((a, b) => {
const isAsc = direction === 'asc';
switch (active) {
case 'name':
return compare(a.name, b.name, isAsc);
case 'enabled':
return compare(a.enabled, b.enabled, isAsc);
case 'creationDate':
return compare(a.creationDate, b.creationDate, isAsc);
case 'lastDate':
return compare(a.lastDate ?? 0, b.lastDate ?? 0, isAsc);
default:
return 0;
}
});
});
dataSource = computed(() => {
const data = this.sortedList();
const pageIndex = this.pageIndex();
const pageSize = this.pageSize();
return data.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
});
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
@ViewChild(MatSort, { static: true }) sort!: MatSort;
list: Client[] = [];
dataSource: ClientListDataSource = new ClientListDataSource(this.list, this.paginator, this.sort);
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['code', 'name', 'enabled', 'otp', 'created', 'last']; displayedColumns = ['code', 'name', 'enabled', 'otp', 'created', 'last'];
handlePageEvent(e: PageEvent) {
this.pageSize.set(e.pageSize);
this.pageIndex.set(e.pageIndex);
}
ngOnInit() { sortData(sort: Sort) {
this.route.data.subscribe((value) => { this.sortActive.set(sort.active);
const data = value as { list: Client[] }; this.sortDirection.set(sort.direction);
this.list = data.list;
});
this.dataSource = new ClientListDataSource(this.list, this.paginator, this.sort);
} }
} }
const compare = (a: string | number | boolean, b: string | number | boolean, isAsc: boolean) =>
(a < b ? -1 : 1) * (isAsc ? 1 : -1);
@@ -1,18 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { Client } from './client';
import { clientResolver } from './client.resolver';
describe('clientResolver', () => {
const executeResolver: ResolveFn<Client> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => clientResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});
@@ -1,10 +0,0 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { Client } from './client';
import { ClientService } from './client.service';
export const clientResolver: ResolveFn<Client> = (route) => {
const id = route.paramMap.get('id');
return inject(ClientService).get(id);
};
-8
View File
@@ -2,9 +2,7 @@ import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service'; import { authGuard } from '../auth/auth-guard.service';
import { ClientDetailComponent } from './client-detail/client-detail.component'; import { ClientDetailComponent } from './client-detail/client-detail.component';
import { clientListResolver } from './client-list.resolver';
import { ClientListComponent } from './client-list/client-list.component'; import { ClientListComponent } from './client-list/client-list.component';
import { clientResolver } from './client.resolver';
export const routes: Routes = [ export const routes: Routes = [
{ {
@@ -14,9 +12,6 @@ export const routes: Routes = [
data: { data: {
permission: 'Cost Centres', permission: 'Cost Centres',
}, },
resolve: {
list: clientListResolver,
},
}, },
{ {
path: ':id', path: ':id',
@@ -25,8 +20,5 @@ export const routes: Routes = [
data: { data: {
permission: 'Cost Centres', permission: 'Cost Centres',
}, },
resolve: {
item: clientResolver,
},
}, },
]; ];
+11 -15
View File
@@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient, httpResource } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, Signal } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@@ -7,8 +7,6 @@ import { ErrorLoggerService } from '../core/error-logger.service';
import { Client } from './client'; import { Client } from './client';
const url = '/api/clients'; const url = '/api/clients';
const serviceName = 'ClientService';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@@ -16,28 +14,26 @@ export class ClientService {
private http = inject(HttpClient); private http = inject(HttpClient);
private log = inject(ErrorLoggerService); private log = inject(ErrorLoggerService);
get(id: string | null): Observable<Client> { get(id: Signal<string | null>) {
const getUrl: string = id === null ? url : `${url}/${id}`; return httpResource<Client>(() => {
return this.http const getUrl: string = id() === null ? url : `${url}/${id()}`;
.get<Client>(getUrl) return getUrl;
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Client>; });
} }
list(): Observable<Client[]> { list() {
return this.http return httpResource<Client[]>(() => `${url}/list`);
.get<Client[]>(`${url}/list`)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Client[]>;
} }
update(client: Client): Observable<Client> { update(client: Client): Observable<Client> {
return this.http return this.http
.put<Client>(`${url}/${client.id}`, client) .put<Client>(`${url}/${client.id}`, client)
.pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable<Client>; .pipe(catchError(this.log.handleError('ClientService', 'update'))) as Observable<Client>;
} }
delete(id: string): Observable<Client> { delete(id: string): Observable<Client> {
return this.http return this.http
.delete<Client>(`${url}/${id}`) .delete<Client>(`${url}/${id}`)
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<Client>; .pipe(catchError(this.log.handleError('ClientService', 'delete'))) as Observable<Client>;
} }
} }
@@ -1,73 +0,0 @@
import { DataSource } from '@angular/cdk/collections';
import { EventEmitter } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort, Sort } from '@angular/material/sort';
import { merge, Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
import { ClosingStockItem } from './closing-stock-item';
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
const compare = (a: string | number, b: string | number, isAsc: boolean) => (a < b ? -1 : 1) * (isAsc ? 1 : -1);
export class ClosingStockDataSource extends DataSource<ClosingStockItem> {
constructor(
public data: ClosingStockItem[],
private paginator?: MatPaginator,
private sort?: MatSort,
) {
super();
}
connect(): Observable<ClosingStockItem[]> {
const dataMutations: (EventEmitter<PageEvent> | EventEmitter<Sort>)[] = [];
if (this.paginator) {
dataMutations.push((this.paginator as MatPaginator).page);
}
if (this.sort) {
dataMutations.push((this.sort as MatSort).sortChange);
}
// Set the paginators length
if (this.paginator) {
this.paginator.length = this.data.length;
}
return merge(observableOf(this.data), ...dataMutations).pipe(
map(() => this.getPagedData(this.getSortedData([...this.data]))),
);
}
disconnect() {}
private getPagedData(data: ClosingStockItem[]) {
if (this.paginator === undefined) {
return data;
}
const startIndex = this.paginator.pageIndex * this.paginator.pageSize;
return data.splice(startIndex, this.paginator.pageSize);
}
private getSortedData(data: ClosingStockItem[]) {
if (this.sort === undefined) {
return data;
}
if (!this.sort.active || this.sort.direction === '') {
return data;
}
const sort = this.sort as MatSort;
return data.sort((a, b) => {
const isAsc = sort.direction === 'asc';
switch (sort.active) {
case 'product':
return compare(`${a.group} - ${a.product}`, `${b.group} - ${b.product}`, isAsc);
case 'quantity':
return compare(+a.quantity, +b.quantity, isAsc);
case 'amount':
return compare(+a.amount, +b.amount, isAsc);
default:
return 0;
}
});
}
}
@@ -1,18 +1,18 @@
<h2 class="row-container space-between"> <h2 class="row-container space-between">
<span>Closing Stock</span> <span>Closing Stock</span>
@if (dataSource.data.length) { @if ((this.info().items ?? []).length) {
<button mat-icon-button (click)="exportCsv()"> <button mat-icon-button (click)="exportCsv()">
<mat-icon>save_alt</mat-icon> <mat-icon>save_alt</mat-icon>
</button> </button>
} }
</h2> </h2>
<form [formGroup]="form" class="flex-col"> <form class="flex-col" [formRoot]="form">
<div class="row-container"> <div class="row-container">
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Department</mat-label> <mat-label>Department</mat-label>
<mat-select formControlName="costCentre" name="costCentre"> <mat-select [formField]="form.costCentre">
@for (at of costCentres; track at) { @for (at of costCentres(); track at) {
<mat-option [value]="at.id"> <mat-option [value]="at.id">
{{ at.name }} {{ at.name }}
</mat-option> </mat-option>
@@ -21,96 +21,98 @@
</mat-form-field> </mat-form-field>
<mat-form-field class="flex-auto basis-2-5"> <mat-form-field class="flex-auto basis-2-5">
<mat-label>Date</mat-label> <mat-label>Date</mat-label>
<input matInput #dateElement [matDatepicker]="dateInput" formControlName="date" autocomplete="off" /> <input matInput #dateElement [matDatepicker]="dateInput" [formField]="form.date" autocomplete="off" />
<mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="dateInput"></mat-datepicker-toggle>
<mat-datepicker #dateInput></mat-datepicker> <mat-datepicker #dateInput></mat-datepicker>
</mat-form-field> </mat-form-field>
<button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button> <button mat-raised-button color="primary" (click)="show()" class="flex-auto basis-1-5">Show</button>
</div> </div>
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements" formArrayName="stocks"> @if (infoResource.isLoading()) {
<!-- Product Column --> <app-skeleton-loader type="table" [count]="1" />
<ng-container matColumnDef="product" class="first"> } @else if (infoResource.error()) {
<mat-header-cell *matHeaderCellDef mat-sort-header class="first">Product</mat-header-cell> <app-error-state [message]="'Failed to load data.'" (retryAction)="infoResource.reload()" />
<mat-cell *matCellDef="let row">{{ row.product.name }}</mat-cell> } @else {
</ng-container> <mat-table #table [dataSource]="dataSource()" matSort (matSortChange)="sortData($event)">
<!-- Product Column -->
<ng-container matColumnDef="product" class="first">
<mat-header-cell *matHeaderCellDef mat-sort-header class="first">Product</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.product.name }}</mat-cell>
</ng-container>
<!-- Group Column --> <!-- Group Column -->
<ng-container matColumnDef="group"> <ng-container matColumnDef="group">
<mat-header-cell *matHeaderCellDef class="middle">Group</mat-header-cell> <mat-header-cell *matHeaderCellDef class="middle">Group</mat-header-cell>
<mat-cell *matCellDef="let row" class="middle">{{ row.group }}</mat-cell> <mat-cell *matCellDef="let row" class="middle">{{ row.group }}</mat-cell>
</ng-container> </ng-container>
<!-- Quantity Column --> <!-- Quantity Column -->
<ng-container matColumnDef="quantity"> <ng-container matColumnDef="quantity">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right middle">Closing Stock</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header class="right middle">Closing Stock</mat-header-cell>
<mat-cell *matCellDef="let row" class="right middle">{{ row.quantity | number: '0.2-2' }}</mat-cell> <mat-cell *matCellDef="let row" class="right middle">{{ row.quantity | number: '0.2-2' }}</mat-cell>
</ng-container> </ng-container>
<!-- Physical Column --> <!-- Physical Column -->
<ng-container matColumnDef="physical"> <ng-container matColumnDef="physical">
<mat-header-cell *matHeaderCellDef class="middle">Physical Stock</mat-header-cell> <mat-header-cell *matHeaderCellDef class="middle">Physical Stock</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle"> <mat-cell *matCellDef="let row; let i = index" class="middle">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Physical</mat-label> <mat-label>Physical</mat-label>
<input matInput type="number" formControlName="physical" (change)="updatePhysical($event, row)" /> <input matInput type="number" formControlName="physical" (change)="updatePhysical($event, row)" />
</mat-form-field> </mat-form-field>
</mat-cell> </mat-cell>
</ng-container> </ng-container>
<!-- Variance Column --> <!-- Variance Column -->
<ng-container matColumnDef="variance" class="middle"> <ng-container matColumnDef="variance" class="middle">
<mat-header-cell *matHeaderCellDef class="right">Variance</mat-header-cell> <mat-header-cell *matHeaderCellDef class="right">Variance</mat-header-cell>
<mat-cell *matCellDef="let row" class="right middle">{{ <mat-cell *matCellDef="let row" class="right middle">{{
row.quantity - row.physical | number: '0.2-2' row.quantity - row.physical | number: '0.2-2'
}}</mat-cell> }}</mat-cell>
</ng-container> </ng-container>
<!-- Department Column --> <!-- Department Column -->
<ng-container matColumnDef="department"> <ng-container matColumnDef="department">
<mat-header-cell *matHeaderCellDef class="middle">Department</mat-header-cell> <mat-header-cell *matHeaderCellDef class="middle">Department</mat-header-cell>
<mat-cell *matCellDef="let row; let i = index" [formGroupName]="i" class="middle"> <mat-cell *matCellDef="let row; let i = index" class="middle">
<mat-form-field class="flex-auto"> <mat-form-field class="flex-auto">
<mat-label>Department</mat-label> <mat-label>Department</mat-label>
<mat-select <mat-select [formField]="form.costCentre" (selectionChange)="updateDepartment($event.value, row)">
formControlName="costCentre" @for (at of costCentres(); track at) {
name="costCentre" <mat-option [value]="at.id">
(selectionChange)="updateDepartment($event.value, row)" {{ at.name }}
> </mat-option>
@for (at of costCentres; track at) { }
<mat-option [value]="at.id"> </mat-select>
{{ at.name }} </mat-form-field>
</mat-option> </mat-cell>
} </ng-container>
</mat-select>
</mat-form-field>
</mat-cell>
</ng-container>
<!-- Amount Column --> <!-- Amount Column -->
<ng-container matColumnDef="amount" class="last"> <ng-container matColumnDef="amount" class="last">
<mat-header-cell *matHeaderCellDef mat-sort-header class="right last">Amount</mat-header-cell> <mat-header-cell *matHeaderCellDef mat-sort-header class="right last">Amount</mat-header-cell>
<mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' }}</mat-cell> <mat-cell *matCellDef="let row" class="right">{{ row.amount | currency: 'INR' }}</mat-cell>
</ng-container> </ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row> <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row> <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table> </mat-table>
<mat-paginator <mat-paginator
#paginator (page)="handlePageEvent($event)"
[length]="dataSource.data.length" [length]="(this.info().items ?? []).length"
[pageIndex]="0" [pageIndex]="pageIndex()"
[pageSize]="50" [pageSize]="pageSize()"
[pageSizeOptions]="[25, 50, 100, 250, 300, 5000]" [pageSizeOptions]="[25, 50, 100, 250, 300, 5000]"
> >
</mat-paginator> </mat-paginator>
}
</form> </form>
<div class="row-container"> <div class="row-container">
<button mat-raised-button color="primary" (click)="save()" [disabled]="form.pristine">Save</button> <button mat-raised-button color="primary" (click)="save()" [disabled]="!$any(form).dirty">Save</button>
@if (canDelete()) { @if (canDelete()) {
<button mat-raised-button (click)="post()" [disabled]="info.posted || !auth.allowed('post-vouchers')"> <button mat-raised-button (click)="post()" [disabled]="info().posted || !auth.allowed('post-vouchers')">
{{ info.posted ? 'Posted' : 'Post' }} {{ info().posted ? 'Posted' : 'Post' }}
</button> </button>
<button mat-raised-button color="warn" (click)="confirmDelete()" [disabled]="!canSave()">Delete</button> <button mat-raised-button color="warn" (click)="confirmDelete()" [disabled]="!canSave()">Delete</button>
} }
@@ -1,6 +1,6 @@
import { CurrencyPipe, DecimalPipe } from '@angular/common'; import { CurrencyPipe, DecimalPipe } from '@angular/common';
import { Component, ElementRef, HostListener, inject, OnInit, ViewChild } from '@angular/core'; import { Component, HostListener, inject, computed, input, linkedSignal, signal, afterNextRender } from '@angular/core';
import { FormArray, FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { form as createForm, FormField, FormRoot } from '@angular/forms/signals';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core'; import { MatOptionModule } from '@angular/material/core';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
@@ -8,31 +8,40 @@ import { MatDialog } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon'; import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatSelectChange, MatSelectModule } from '@angular/material/select'; import { MatSelectChange, MatSelectModule } from '@angular/material/select';
import { MatSnackBar } from '@angular/material/snack-bar'; import { MatSnackBar } from '@angular/material/snack-bar';
import { MatSort, MatSortModule } from '@angular/material/sort'; import { MatSortModule, Sort } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router'; import { Router } from '@angular/router';
import moment from 'moment'; import moment from 'moment';
import { AuthService } from '../auth/auth.service'; import { AuthService } from '../auth/auth.service';
import { CostCentre } from '../core/cost-centre'; import { CostCentre } from '../core/cost-centre';
import { User } from '../core/user'; import { User } from '../core/user';
import { CostCentreService } from '../cost-centre/cost-centre.service';
import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.component'; import { ConfirmDialogComponent } from '../shared/confirm-dialog/confirm-dialog.component';
import { ErrorStateComponent } from '../shared/error-state/error-state.component';
import { SkeletonLoaderComponent } from '../shared/skeleton-loader/skeleton-loader.component';
import { ToCsvService } from '../shared/to-csv.service'; import { ToCsvService } from '../shared/to-csv.service';
import { ClosingStock } from './closing-stock'; import { ClosingStock } from './closing-stock';
import { ClosingStockDataSource } from './closing-stock-datasource';
import { ClosingStockItem } from './closing-stock-item'; import { ClosingStockItem } from './closing-stock-item';
import { ClosingStockService } from './closing-stock.service'; import { ClosingStockService } from './closing-stock.service';
export interface ClosingStockFormData {
date: moment.Moment;
costCentre: CostCentre | null;
stocks: { physical: number; costCentre: string | undefined }[];
}
@Component({ @Component({
selector: 'app-closing-stock', selector: 'app-closing-stock',
templateUrl: './closing-stock.component.html', templateUrl: './closing-stock.component.html',
styleUrls: ['./closing-stock.component.css'], styleUrls: ['./closing-stock.component.css'],
imports: [ imports: [
MatIconModule, MatIconModule,
ReactiveFormsModule, FormField,
FormRoot,
MatFormFieldModule, MatFormFieldModule,
MatSelectModule, MatSelectModule,
MatOptionModule, MatOptionModule,
@@ -44,75 +53,92 @@ import { ClosingStockService } from './closing-stock.service';
MatPaginatorModule, MatPaginatorModule,
DecimalPipe, DecimalPipe,
CurrencyPipe, CurrencyPipe,
SkeletonLoaderComponent,
ErrorStateComponent,
], ],
}) })
export class ClosingStockComponent implements OnInit { export class ClosingStockComponent {
private route = inject(ActivatedRoute);
private router = inject(Router); private router = inject(Router);
private toCsv = inject(ToCsvService); private toCsv = inject(ToCsvService);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private snackBar = inject(MatSnackBar); private snackBar = inject(MatSnackBar);
auth = inject(AuthService); protected auth = inject(AuthService);
private ser = inject(ClosingStockService); private ser = inject(ClosingStockService);
private costCentreSer = inject(CostCentreService);
pageSize = signal(50);
pageIndex = signal(0);
sortActive = signal('');
sortDirection = signal('');
id = input(null, { transform: (v: string | null | undefined) => v ?? null });
d = input(null, { transform: (v: string | null | undefined) => v ?? null });
startDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
finishDate = input(null, { transform: (v: string | null | undefined) => v ?? null });
infoResource = this.ser.list(this.id, this.d);
info = computed(() => this.infoResource.value() ?? new ClosingStock());
costCentresResource = this.costCentreSer.list();
costCentres = computed(() => this.costCentresResource.value() ?? []);
model = linkedSignal({
source: computed(() => ({ info: this.info(), costCentres: this.costCentres() })),
computation: ({ info, costCentres }): ClosingStockFormData => ({
date: moment(info.date, 'DD-MMM-YYYY'),
costCentre: costCentres.find((c) => c.id === info.costCentre?.id) || null,
stocks: info.items.map((x) => ({
physical: x.physical,
costCentre: x.costCentre?.id,
})),
}),
});
form = createForm(this.model);
sortedList = computed(() => {
const data = this.info().items ?? [];
const active = this.sortActive();
const direction = this.sortDirection();
if (!active || direction === '') {
return data;
}
return [...data].sort((a, b) => {
const isAsc = direction === 'asc';
switch (active) {
case 'product':
return compare(a.product?.name ?? '', b.product?.name ?? '', isAsc);
case 'group':
return compare(a.group ?? '', b.group ?? '', isAsc);
case 'quantity':
return compare(a.quantity ?? 0, b.quantity ?? 0, isAsc);
case 'amount':
return compare(a.amount ?? 0, b.amount ?? 0, isAsc);
default:
return 0;
}
});
});
dataSource = computed(() => {
const data = this.sortedList();
const pageIndex = this.pageIndex();
const pageSize = this.pageSize();
return data.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize);
});
@ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator;
@ViewChild(MatSort, { static: true }) sort!: MatSort;
@ViewChild('dateElement', { static: true }) dateElement!: ElementRef<HTMLInputElement>;
info: ClosingStock = new ClosingStock();
dataSource: ClosingStockDataSource = new ClosingStockDataSource(this.info.items, this.paginator, this.sort);
form: FormGroup<{
date: FormControl<moment.Moment>;
costCentre: FormControl<string | null>;
stocks: FormArray<
FormGroup<{
physical: FormControl<number>;
costCentre: FormControl<string | undefined>;
}>
>;
}>;
/** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */ /** Columns displayed in the table. Columns IDs can be added, removed, or reordered. */
displayedColumns = ['product', 'group', 'quantity', 'physical', 'variance', 'department', 'amount']; displayedColumns = ['product', 'group', 'quantity', 'physical', 'variance', 'department', 'amount'];
costCentres: CostCentre[];
@HostListener('window:keydown.f2', ['$event']) @HostListener('window:keydown.f2', ['$event'])
focusDate(event: Event) { focusDate(event: Event) {
event.preventDefault(); event.preventDefault();
this.dateElement.nativeElement.focus(); this.form().focusBoundControl();
this.dateElement.nativeElement.select();
} }
constructor() { constructor() {
this.costCentres = []; afterNextRender(() => {
this.form = new FormGroup({ this.form().focusBoundControl();
date: new FormControl(moment(new Date()), { nonNullable: true }),
costCentre: new FormControl<string | null>(null),
stocks: new FormArray<FormGroup<{ physical: FormControl<number>; costCentre: FormControl<string | undefined> }>>(
[],
),
});
}
ngOnInit() {
this.route.data.subscribe((value) => {
const data = value as { info: ClosingStock; costCentres: CostCentre[] };
this.info = data.info;
this.costCentres = data.costCentres;
this.form.patchValue({
date: moment(this.info.date, 'DD-MMM-YYYY'),
costCentre: this.info.costCentre.id,
});
this.form.controls.stocks.clear();
this.info.items.forEach((x) =>
this.form.controls.stocks.push(
new FormGroup({
physical: new FormControl(x.physical, { nonNullable: true }),
costCentre: new FormControl(x.costCentre?.id, { nonNullable: true }),
}),
),
);
this.dataSource = new ClosingStockDataSource(this.info.items, this.paginator, this.sort);
}); });
} }
@@ -137,25 +163,23 @@ export class ClosingStockComponent implements OnInit {
} }
getClosingStock(): ClosingStock { getClosingStock(): ClosingStock {
const formModel = this.form.value; const formModel = this.model();
this.info.date = moment(formModel.date).format('DD-MMM-YYYY'); const currentInfo = this.info();
const array = this.form.controls.stocks; currentInfo.date = formModel.date.format('DD-MMM-YYYY');
this.info.items.forEach((item, index) => { const array = formModel.stocks;
item.physical = +(array.controls[index].value.physical ?? ''); currentInfo.items.forEach((item, index) => {
item.costCentre = item.physical = array[index].physical ?? 0;
array.controls[index].value.costCentre == null item.costCentre = array[index].costCentre == null ? undefined : new CostCentre({ id: array[index].costCentre });
? undefined
: new CostCentre({ id: array.controls[index].value.costCentre });
}); });
return this.info; return currentInfo;
} }
getInfo(): ClosingStock { getInfo(): ClosingStock {
const formModel = this.form.value; const formModel = this.model();
return new ClosingStock({ return new ClosingStock({
date: moment(formModel.date).format('DD-MMM-YYYY'), date: formModel.date.format('DD-MMM-YYYY'),
costCentre: new CostCentre({ id: formModel.costCentre ?? '' }), costCentre: new CostCentre({ id: formModel.costCentre?.id ?? '' }),
}); });
} }
@@ -167,7 +191,7 @@ export class ClosingStockComponent implements OnInit {
Amount: 'amount', Amount: 'amount',
}; };
const d = JSON.parse(JSON.stringify(this.dataSource.data)).map((x: ClosingStockItem) => ({ const d = JSON.parse(JSON.stringify(this.dataSource())).map((x: ClosingStockItem) => ({
product: x.product.name, product: x.product.name,
group: x.group, group: x.group,
quantity: x.quantity, quantity: x.quantity,
@@ -193,21 +217,21 @@ export class ClosingStockComponent implements OnInit {
} }
canDelete() { canDelete() {
return this.info.items.find((x) => !!x.id) !== undefined; return this.info().items.find((x) => !!x.id) !== undefined;
} }
canSave() { canSave() {
if (this.info.items.find((x) => !!x.id) !== undefined) { if (this.info().items.find((x) => !!x.id) !== undefined) {
return true; return true;
} }
if (this.info.posted && this.auth.allowed('edit-posted-vouchers')) { if (this.info().posted && this.auth.allowed('edit-posted-vouchers')) {
return true; return true;
} }
return this.info.user.id === (this.auth.user as User).id || this.auth.allowed("edit-other-user's-vouchers"); return this.info().user.id === (this.auth.user as User).id || this.auth.allowed("edit-other-user's-vouchers");
} }
post() { post() {
this.ser.post(this.info.date, this.info.costCentre.id as string).subscribe({ this.ser.post(this.info().date, this.info().costCentre.id as string).subscribe({
next: () => { next: () => {
this.snackBar.open('Voucher Posted', 'Success'); this.snackBar.open('Voucher Posted', 'Success');
}, },
@@ -218,7 +242,7 @@ export class ClosingStockComponent implements OnInit {
} }
delete() { delete() {
this.ser.delete(this.info.date, this.info.costCentre.id as string).subscribe({ this.ser.delete(this.info().date, this.info().costCentre.id as string).subscribe({
next: () => { next: () => {
this.snackBar.open('', 'Success'); this.snackBar.open('', 'Success');
this.router.navigate(['/closing-stock'], { replaceUrl: true }); this.router.navigate(['/closing-stock'], { replaceUrl: true });
@@ -244,4 +268,16 @@ export class ClosingStockComponent implements OnInit {
} }
}); });
} }
handlePageEvent(e: PageEvent) {
this.pageSize.set(e.pageSize);
this.pageIndex.set(e.pageIndex);
}
sortData(sort: Sort) {
this.sortActive.set(sort.active);
this.sortDirection.set(sort.direction);
}
} }
const compare = (a: string | number | boolean, b: string | number | boolean, isAsc: boolean) =>
(a < b ? -1 : 1) * (isAsc ? 1 : -1);
@@ -1,18 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { ResolveFn } from '@angular/router';
import { ClosingStock } from './closing-stock';
import { closingStockResolver } from './closing-stock.resolver';
describe('closingStockResolver', () => {
const executeResolver: ResolveFn<ClosingStock> = (...resolverParameters) =>
TestBed.runInInjectionContext(() => closingStockResolver(...resolverParameters));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(executeResolver).toBeTruthy();
});
});
@@ -1,11 +0,0 @@
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ClosingStock } from './closing-stock';
import { ClosingStockService } from './closing-stock.service';
export const closingStockResolver: ResolveFn<ClosingStock> = (route) => {
const date = route.paramMap.get('date');
const costCentre = route.queryParamMap.get('d') || null;
return inject(ClosingStockService).list(date, costCentre);
};
@@ -1,9 +1,7 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { authGuard } from '../auth/auth-guard.service'; import { authGuard } from '../auth/auth-guard.service';
import { costCentreListResolver } from '../cost-centre/cost-centre-list.resolver';
import { ClosingStockComponent } from './closing-stock.component'; import { ClosingStockComponent } from './closing-stock.component';
import { closingStockResolver } from './closing-stock.resolver';
export const routes: Routes = [ export const routes: Routes = [
{ {
@@ -13,10 +11,6 @@ export const routes: Routes = [
data: { data: {
permission: 'Closing Stock', permission: 'Closing Stock',
}, },
resolve: {
info: closingStockResolver,
costCentres: costCentreListResolver,
},
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
{ {
@@ -26,10 +20,6 @@ export const routes: Routes = [
data: { data: {
permission: 'Closing Stock', permission: 'Closing Stock',
}, },
resolve: {
info: closingStockResolver,
costCentres: costCentreListResolver,
},
runGuardsAndResolvers: 'always', runGuardsAndResolvers: 'always',
}, },
]; ];
@@ -1,5 +1,5 @@
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, httpResource, HttpParams } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, Signal } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@@ -7,8 +7,6 @@ import { ErrorLoggerService } from '../core/error-logger.service';
import { ClosingStock } from './closing-stock'; import { ClosingStock } from './closing-stock';
const url = '/api/closing-stock'; const url = '/api/closing-stock';
const serviceName = 'ClosingStockService';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@@ -16,34 +14,33 @@ export class ClosingStockService {
private http = inject(HttpClient); private http = inject(HttpClient);
private log = inject(ErrorLoggerService); private log = inject(ErrorLoggerService);
list(date: string | null, costCentre: string | null): Observable<ClosingStock> { list(date: Signal<string | null>, costCentre: Signal<string | null>) {
const listUrl = date === null ? url : `${url}/${date}`; return httpResource<ClosingStock>(() => {
const options = { params: new HttpParams() }; const listUrl = date() === null ? url : `${url}/${date()}`;
if (costCentre !== null) { const params: Record<string, string> = {};
options.params = options.params.set('d', costCentre); const costCentre_val = costCentre();
} if (costCentre_val !== null) params['d'] = costCentre_val;
return this.http return { url: listUrl, params };
.get<ClosingStock>(listUrl, options) });
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<ClosingStock>;
} }
save(closingStock: ClosingStock): Observable<ClosingStock> { save(closingStock: ClosingStock): Observable<ClosingStock> {
return this.http return this.http
.post<ClosingStock>(url, closingStock) .post<ClosingStock>(url, closingStock)
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<ClosingStock>; .pipe(catchError(this.log.handleError('ClosingStockService', 'save'))) as Observable<ClosingStock>;
} }
post(date: string, costCentre: string): Observable<ClosingStock> { post(date: string, costCentre: string): Observable<ClosingStock> {
const options = { params: new HttpParams().set('d', costCentre) }; const options = { params: new HttpParams().set('d', costCentre) };
return this.http return this.http
.post<ClosingStock>(`${url}/${date}`, {}, options) .post<ClosingStock>(`${url}/${date}`, {}, options)
.pipe(catchError(this.log.handleError(serviceName, 'Post Voucher'))) as Observable<ClosingStock>; .pipe(catchError(this.log.handleError('ClosingStockService', 'Post Voucher'))) as Observable<ClosingStock>;
} }
delete(date: string, costCentre: string): Observable<ClosingStock> { delete(date: string, costCentre: string): Observable<ClosingStock> {
const options = { params: new HttpParams().set('d', costCentre) }; const options = { params: new HttpParams().set('d', costCentre) };
return this.http return this.http
.delete<ClosingStock>(`${url}/${date}`, options) .delete<ClosingStock>(`${url}/${date}`, options)
.pipe(catchError(this.log.handleError(serviceName, 'Delete Voucher'))) as Observable<ClosingStock>; .pipe(catchError(this.log.handleError('ClosingStockService', 'Delete Voucher'))) as Observable<ClosingStock>;
} }
} }
+28 -36
View File
@@ -1,5 +1,5 @@
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, httpResource } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, Signal } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@@ -8,50 +8,40 @@ import { AccountBalance } from './account-balance';
import { ErrorLoggerService } from './error-logger.service'; import { ErrorLoggerService } from './error-logger.service';
const url = '/api/accounts'; const url = '/api/accounts';
const serviceName = 'AccountService';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class AccountService { export class AccountService {
private http = inject(HttpClient); private http = inject(HttpClient);
private log = inject(ErrorLoggerService); private log = inject(ErrorLoggerService);
get(id: string | null): Observable<Account> { get(id: Signal<string | null>) {
const getUrl: string = id === null ? `${url}` : `${url}/${id}`; return httpResource<Account>(() => {
return this.http const getUrl: string = id() === null ? `${url}` : `${url}/${id()}`;
.get<Account>(getUrl) return getUrl;
.pipe(catchError(this.log.handleError(serviceName, `get id=${id}`))) as Observable<Account>; });
} }
list(): Observable<Account[]> { list() {
return this.http return httpResource<Account[]>(() => `${url}/list`);
.get<Account[]>(`${url}/list`)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Account[]>;
} }
paymentAutocomplete(query: string): Observable<Account[]> { paymentAutocomplete(query: Signal<string>) {
const options = { params: new HttpParams().set('q', query).set('t', '1') }; return httpResource<Account[]>(() => ({ url: `${url}/query`, params: { q: query(), t: '1' } }));
return this.http
.get<Account[]>(`${url}/query`, options)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Account[]>;
} }
receiptAutocomplete(query: string): Observable<Account[]> { receiptAutocomplete(query: Signal<string>) {
const options = { params: new HttpParams().set('q', query).set('t', '1') }; return httpResource<Account[]>(() => ({ url: `${url}/query`, params: { q: query(), t: '1' } }));
return this.http
.get<Account[]>(`${url}/query`, options)
.pipe(catchError(this.log.handleError(serviceName, 'list'))) as Observable<Account[]>;
} }
save(account: Account): Observable<null> { save(account: Account): Observable<null> {
return this.http return this.http
.post<null>(`${url}`, account) .post<null>(`${url}`, account)
.pipe(catchError(this.log.handleError(serviceName, 'save'))) as Observable<null>; .pipe(catchError(this.log.handleError('AccountService', 'save'))) as Observable<null>;
} }
update(account: Account): Observable<null> { update(account: Account): Observable<null> {
return this.http return this.http
.put<null>(`${url}/${account.id}`, account) .put<null>(`${url}/${account.id}`, account)
.pipe(catchError(this.log.handleError(serviceName, 'update'))) as Observable<null>; .pipe(catchError(this.log.handleError('AccountService', 'update'))) as Observable<null>;
} }
saveOrUpdate(account: Account): Observable<null> { saveOrUpdate(account: Account): Observable<null> {
@@ -64,20 +54,22 @@ export class AccountService {
delete(id: string): Observable<Account> { delete(id: string): Observable<Account> {
return this.http return this.http
.delete<Account>(`${url}/${id}`) .delete<Account>(`${url}/${id}`)
.pipe(catchError(this.log.handleError(serviceName, 'delete'))) as Observable<Account>; .pipe(catchError(this.log.handleError('AccountService', 'delete'))) as Observable<Account>;
} }
autocomplete(query: string): Observable<Account[]> { autocomplete(query: Signal<string | null>) {
const options = { params: new HttpParams().set('q', query) }; return httpResource<Account[]>(() => {
return this.http const q = query();
.get<Account[]>(`${url}/query`, options) return q ? `${url}/query?q=${encodeURIComponent(q)}` : undefined;
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<Account[]>; });
} }
balance(id: string, date: string): Observable<AccountBalance> { balance(id: Signal<string | null>, date: Signal<string | null>) {
const options = { params: new HttpParams().set('d', date) }; return httpResource<AccountBalance>(() => {
return this.http const accId = id();
.get<AccountBalance>(`${url}/${id}/balance`, options) const d = date();
.pipe(catchError(this.log.handleError(serviceName, 'balance'))) as Observable<AccountBalance>; if (!accId || !d) return undefined;
return `${url}/${accId}/balance?d=${encodeURIComponent(d)}`;
});
} }
} }
+9 -11
View File
@@ -1,23 +1,21 @@
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, httpResource, HttpParams } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, Signal } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
import { Batch } from './batch'; import { Batch } from './batch';
import { ErrorLoggerService } from './error-logger.service'; import { ErrorLoggerService } from './error-logger.service';
const url = '/api/batch'; const url = '/api/batch';
const serviceName = 'BatchService';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class BatchService { export class BatchService {
private http = inject(HttpClient); private http = inject(HttpClient);
private log = inject(ErrorLoggerService); private log = inject(ErrorLoggerService);
autocomplete(date: string, term: string): Observable<Batch[]> { autocomplete(date: Signal<string | null>, term: Signal<string | null>) {
const options = { params: new HttpParams().set('q', term).set('d', date) }; return httpResource<Batch[]>(() => {
return this.http const q = term();
.get<Batch[]>(url, options) const d = date();
.pipe(catchError(this.log.handleError(serviceName, 'autocomplete'))) as Observable<Batch[]>; if (!q || !d) return undefined;
return { url, params: new HttpParams().set('q', q).set('d', d) };
});
} }
} }
@@ -0,0 +1,7 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { timer, switchMap } from 'rxjs';
export const delayInterceptor: HttpInterceptorFn = (req, next) => {
// Delay all requests by 1500ms to test loading states
return timer(1500).pipe(switchMap(() => next(req)));
};
@@ -1,6 +1,6 @@
<mat-toolbar class="fixed-to-top row-container"> <mat-toolbar class="fixed-to-top row-container">
<span class="" <span class=""
><a routerLink="/">{{ title }}</a></span ><a routerLink="/">{{ title() }}</a></span
> >
<mat-menu #voucherMenu="matMenu"> <mat-menu #voucherMenu="matMenu">
<a mat-menu-item routerLink="/journal">Journal - F7</a> <a mat-menu-item routerLink="/journal">Journal - F7</a>
@@ -1,5 +1,5 @@
import { AsyncPipe } from '@angular/common'; import { AsyncPipe } from '@angular/common';
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject, computed } from '@angular/core';
import { MatAnchor, MatButton } from '@angular/material/button'; import { MatAnchor, MatButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon'; import { MatIcon } from '@angular/material/icon';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu'; import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
@@ -16,22 +16,13 @@ import { TitleService } from '../title.service';
styleUrls: ['./nav-bar.component.css'], styleUrls: ['./nav-bar.component.css'],
imports: [MatToolbar, RouterLink, MatMenu, MatMenuItem, MatButton, MatMenuTrigger, MatIcon, MatAnchor, AsyncPipe], imports: [MatToolbar, RouterLink, MatMenu, MatMenuItem, MatButton, MatMenuTrigger, MatIcon, MatAnchor, AsyncPipe],
}) })
export class NavBarComponent implements OnInit { export class NavBarComponent {
private router = inject(Router); private router = inject(Router);
auth = inject(AuthService); auth = inject(AuthService);
titleService = inject(TitleService); titleService = inject(TitleService);
title: string; private titleResource = this.titleService.get();
title = computed(() => this.titleResource.value() ?? environment.title);
constructor() {
this.title = environment.title;
}
ngOnInit() {
this.titleService.get().subscribe((value) => {
this.title = value;
});
}
logout() { logout() {
this.auth.logout(); this.auth.logout();
+4
View File
@@ -1,3 +1,5 @@
import { Product } from './product';
export class ProductSku { export class ProductSku {
id: string; id: string;
name: string; name: string;
@@ -6,6 +8,8 @@ export class ProductSku {
fractionUnits: string; fractionUnits: string;
isRateContracted: boolean; isRateContracted: boolean;
stock?: number;
product?: Product;
public constructor(init?: Partial<ProductSku>) { public constructor(init?: Partial<ProductSku>) {
this.id = ''; this.id = '';
+3
View File
@@ -1,6 +1,7 @@
import { ProductGroup } from './product-group'; import { ProductGroup } from './product-group';
export class StockKeepingUnit { export class StockKeepingUnit {
id: string | null;
units: string; units: string;
fraction: number; fraction: number;
productYield: number; productYield: number;
@@ -8,6 +9,7 @@ export class StockKeepingUnit {
salePrice: number; salePrice: number;
public constructor(init?: Partial<StockKeepingUnit>) { public constructor(init?: Partial<StockKeepingUnit>) {
this.id = null;
this.units = ''; this.units = '';
this.fraction = 1; this.fraction = 1;
this.productYield = 1; this.productYield = 1;
@@ -23,6 +25,7 @@ export class Product {
name: string; name: string;
description: string | undefined; description: string | undefined;
skus: StockKeepingUnit[]; skus: StockKeepingUnit[];
unit?: { name: string };
fractionUnits: string | undefined; fractionUnits: string | undefined;
isActive: boolean; isActive: boolean;
+4 -15
View File
@@ -1,23 +1,12 @@
import { HttpClient } from '@angular/common/http'; import { httpResource } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators';
import { ErrorLoggerService } from './error-logger.service';
const url = '/api/title'; const url = '/api/title';
const serviceName = 'VoucherService';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class TitleService { export class TitleService {
private http = inject(HttpClient); get() {
private log = inject(ErrorLoggerService); return httpResource<string>(() => url);
get(): Observable<string> {
return this.http
.get<string>(url)
.pipe(catchError(this.log.handleError(serviceName, 'Get Title'))) as Observable<string>;
} }
} }
+48 -32
View File
@@ -1,5 +1,5 @@
import { HttpClient, HttpParams } from '@angular/common/http'; import { HttpClient, httpResource } from '@angular/common/http';
import { inject, Injectable } from '@angular/core'; import { inject, Injectable, Signal, ResourceRef } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
@@ -7,8 +7,6 @@ import { ErrorLoggerService } from './error-logger.service';
import { Voucher } from './voucher'; import { Voucher } from './voucher';
const url = '/api'; const url = '/api';
const serviceName = 'VoucherService';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
@@ -31,46 +29,64 @@ export class VoucherService {
return new Blob([ab], { type: mimeString }); return new Blob([ab], { type: mimeString });
} }
get(id: string, type: string): Observable<Voucher> { get(id: Signal<string | undefined>, type: Signal<string>): ResourceRef<Voucher | undefined> {
const endpoint = type.replace(/ /g, '-').toLowerCase(); return httpResource<Voucher>(() => {
return this.http const i = id();
.get<Voucher>(`${url}/${endpoint}/${id}`) if (!i) return undefined;
.pipe(catchError(this.log.handleError(serviceName, 'Get Voucher'))) as Observable<Voucher>; const endpoint = type().replace(/ /g, '-').toLowerCase();
return `${url}/${endpoint}/${i}`;
});
} }
getOfType(type: string, account: string | null, date: string | null): Observable<Voucher> { getOfType(
const endpoint = type.replace(/ /g, '-').toLowerCase(); type: Signal<string>,
const options = { account: Signal<string | null>,
params: new HttpParams(), date: Signal<string | null>,
}; ): ResourceRef<Voucher | undefined> {
if (account !== null) { return httpResource<Voucher>(() => {
options.params = options.params.set('a', account); const endpoint = type().replace(/ /g, '-').toLowerCase();
} const params = new URLSearchParams();
if (date !== null) { if (account() !== null) params.set('a', account()!);
options.params = options.params.set('d', date); if (date() !== null) params.set('d', date()!);
} const p = params.toString();
return this.http return p ? `${url}/${endpoint}?${p}` : `${url}/${endpoint}`;
.get<Voucher>(`${url}/${endpoint}`, options) });
.pipe(catchError(this.log.handleError(serviceName, 'Get Voucher of Type'))) as Observable<Voucher>;
} }
getIncentive(date: string): Observable<Voucher> { getVoucher(
const options = { params: new HttpParams().set('d', date) }; id: Signal<string | null>,
return this.http type: Signal<string>,
.get<Voucher>(`${url}/incentive`, options) account: Signal<string | null>,
.pipe(catchError(this.log.handleError(serviceName, 'Get Incentive'))) as Observable<Voucher>; date: Signal<string | null>,
): ResourceRef<Voucher | undefined> {
return httpResource<Voucher>(() => {
const endpoint = type().replace(/ /g, '-').toLowerCase();
const i = id();
if (i) {
return `${url}/${endpoint}/${i}`;
}
const params = new URLSearchParams();
if (account() !== null) params.set('a', account()!);
if (date() !== null) params.set('d', date()!);
const p = params.toString();
return p ? `${url}/${endpoint}?${p}` : `${url}/${endpoint}`;
});
}
getIncentive(date: Signal<string>): ResourceRef<Voucher | undefined> {
return httpResource<Voucher>(() => `${url}/incentive?d=${date()}`);
} }
post(id: string): Observable<Voucher> { post(id: string): Observable<Voucher> {
return this.http return this.http
.post<Voucher>(`${url}/post-voucher/${id}`, {}) .post<Voucher>(`${url}/post-voucher/${id}`, {})
.pipe(catchError(this.log.handleError(serviceName, 'Post Voucher'))) as Observable<Voucher>; .pipe(catchError(this.log.handleError('VoucherService', 'Post Voucher'))) as Observable<Voucher>;
} }
delete(id: string): Observable<Voucher> { delete(id: string): Observable<Voucher> {
return this.http return this.http
.delete<Voucher>(`${url}/delete/${id}`) .delete<Voucher>(`${url}/delete/${id}`)
.pipe(catchError(this.log.handleError(serviceName, 'Delete Voucher'))) as Observable<Voucher>; .pipe(catchError(this.log.handleError('VoucherService', 'Delete Voucher'))) as Observable<Voucher>;
} }
saveOrUpdate(voucher: Voucher): Observable<Voucher> { saveOrUpdate(voucher: Voucher): Observable<Voucher> {
@@ -93,7 +109,7 @@ export class VoucherService {
fd.append('data', JSON.stringify(voucher)); fd.append('data', JSON.stringify(voucher));
return this.http return this.http
.post<Voucher>(`${url}/${endpoint}`, fd) .post<Voucher>(`${url}/${endpoint}`, fd)
.pipe(catchError(this.log.handleError(serviceName, 'Save Voucher'))) as Observable<Voucher>; .pipe(catchError(this.log.handleError('VoucherService', 'Save Voucher'))) as Observable<Voucher>;
} }
update(voucher: Voucher, endpoint: string): Observable<Voucher> { update(voucher: Voucher, endpoint: string): Observable<Voucher> {
@@ -108,6 +124,6 @@ export class VoucherService {
fd.append('data', JSON.stringify(voucher)); fd.append('data', JSON.stringify(voucher));
return this.http return this.http
.put<Voucher>(`${url}/${endpoint}/${voucher.id}`, fd) .put<Voucher>(`${url}/${endpoint}/${voucher.id}`, fd)
.pipe(catchError(this.log.handleError(serviceName, 'Update Voucher'))) as Observable<Voucher>; .pipe(catchError(this.log.handleError('VoucherService', 'Update Voucher'))) as Observable<Voucher>;
} }
} }
@@ -1,14 +1,21 @@
<h2>Cost Center</h2> <h2>Cost Center</h2>
<form [formGroup]="form" class="flex-col wrapped"> @if (itemResource.isLoading()) {
<div class="row-container"> <app-skeleton-loader type="card" [count]="1" />
<mat-form-field class="flex-auto"> <app-skeleton-loader type="line" [count]="10" />
<mat-label>Name</mat-label> } @else if (itemResource.error()) {
<input matInput #nameElement formControlName="name" (keyup.enter)="save()" /> <app-error-state [message]="'Failed to load data.'" (retryAction)="itemResource.reload()" />
</mat-form-field> } @else {
</div> <form class="flex-col wrapped" [formRoot]="form">
</form> <div class="row-container">
<mat-form-field class="flex-auto">
<mat-label>Name</mat-label>
<input matInput [formField]="form.name" (keyup.enter)="save()" />
</mat-form-field>
</div>
</form>
<div class="row-container"> <div class="row-container">
<button mat-raised-button (click)="save()" color="primary">Save</button> <button mat-raised-button (click)="save()" color="primary">Save</button>
</div> </div>
}
Loaded 100 of 443 files, more files were not shown because too many files have changed in this diff. Show more