diff --git a/README.md b/README.md index 74d45690..b2b00a60 100644 --- a/README.md +++ b/README.md @@ -47,13 +47,15 @@ To learn more about Prompt Sail’s features and capabilities, see * [Langchain SDK -> Azure OpenAI](/examples/langchain_azure_openai.ipynb) * [Langchain SDK -> Azure OpenAI Ada embeddings](/examples/langchain_azure_oai_embeddings.ipynb) - +* [API Reference](https://try-promptsail.azurewebsites.net/api/docs). --> ## Getting started 🚀 -Checkout the documenttion [how to run PromptSail locally via docker.](https://promptsail.com/docs/quick-start-guide/) +The easiest way is to test our demo at **https://try-promptsail.azurewebsites.net/** (every new deployment erases the database) + +Check out the documentation [how to run PromptSail locally via docker.](https://promptsail.com/docs/quick-start-guide/) ### Run Prompt Sail locally via Docker Compose 🐳 diff --git a/backend/Dockerfile b/backend/Dockerfile index 3d2120ee..bf821fc3 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -110,9 +110,8 @@ COPY --from=builder-base $PYSETUP_PATH $PYSETUP_PATH COPY src /src/ COPY static /static/ -COPY provider_price_list.json test_transactions.csv ./ WORKDIR /src EXPOSE 8000 # Run the application CMD uvicorn app:app --proxy-headers --host 0.0.0.0 --port=${PORT:-8000} -#CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "app:app"] \ No newline at end of file +#CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", "app:app"] diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 60406737..2bc4708c 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] -name = "PromptSail" -version = "0.1.0" -description = "Prompt Sail - prompt management and monitoring tool" +name = "PromptSail-backend" +version = "0.5.4" +description = "Prompt Sail - LLM governance, monitoring and analysis system" authors = ["Przemysław Górecki , Krzysztof Sopyła "] [tool.poetry.dependencies] @@ -15,7 +15,7 @@ pydantic-settings = "^2.0.3" gunicorn = "^22.0.0" brotli = "^1.1.0" pandas = "^2.2.1" -lato = "^0.8.0" +lato = "^0.11.0" python-slugify = "^8.0.4" fastapi = "^0.110.1" pyzmq = "~25" diff --git a/backend/src/app/app.py b/backend/src/app/app.py index 945b8af1..d9cb06a1 100644 --- a/backend/src/app/app.py +++ b/backend/src/app/app.py @@ -64,7 +64,7 @@ async def fastapi_lifespan(app: FastAPI): ctx.call(add_project, data2) if settings_repository.count() == 0: - organization_name = os.getenv("ORGANIZATION_NAME", None) + organization_name = os.getenv("ORGANIZATION_NAME", "PromptSail") if organization_name is not None: data = OrganizationSettings( @@ -80,5 +80,10 @@ async def fastapi_lifespan(app: FastAPI): ... -app = FastAPI(lifespan=fastapi_lifespan) +app = FastAPI(lifespan=fastapi_lifespan, + title="PromptSail API", + description="API for PromptSail - prompt management and monitoring tool", + version="0.5.4", + openapi_version="3.1.0", + ) app.container = container diff --git a/backend/src/app/middleware.py b/backend/src/app/middleware.py index ac23ca1d..661b84bd 100644 --- a/backend/src/app/middleware.py +++ b/backend/src/app/middleware.py @@ -27,37 +27,39 @@ # return response -if config.DEBUG: - - @app.middleware("exception_handler") - async def __call__(request: Request, call_next): - """ - Middleware for managing exception handling. - - :param request: The incoming request. - :param call_next: The callable representing the next middleware or endpoint in the chain. - :return: The response from the middleware or endpoint. - """ - logger = get_logger(request) - try: - return await call_next(request) - except HTTPException as http_exception: - return JSONResponse( - status_code=http_exception.status_code, - content={ - "error": "Client Error", - "messages": str(http_exception.detail), - }, - ) - except Exception as e: - logger.exception(f"Error message: {e.__class__.__name__}. Args: {e.args}") - return JSONResponse( - status_code=500, - content={ - "error": "Internal Server Error", - "message": "An unexpected error occurred.", - }, - ) +# if config.DEBUG: + +@app.middleware("exception_handler") +async def __call__(request: Request, call_next): + """ + Middleware for managing exception handling. + + :param request: The incoming request. + :param call_next: The callable representing the next middleware or endpoint in the chain. + :return: The response from the middleware or endpoint. + """ + logger = get_logger(request) + try: + return await call_next(request) + except HTTPException as http_exception: + logger.exception(f"HttpExcepion occures {http_exception.status_code} - {http_exception.detail}") + raise http_exception + # return JSONResponse( + # status_code=http_exception.status_code, + # content={ + # "error": "Client Error", + # "messages": str(http_exception.detail), + # }, + # ) + except Exception as e: + logger.exception(f"Error message: {e.__class__.__name__}. Args: {e.args}") + return JSONResponse( + status_code=500, + content={ + "error": "Internal Server Error", + "message": "An unexpected error occurred.", + }, + ) @app.middleware("transaction_context") @@ -78,38 +80,38 @@ async def __call__(request: Request, call_next): return response -@app.middleware("proxy_tunnel") -async def __call__(request: Request, call_next): - """ - Middleware for handling proxy tunnel requests. - - :param request: The incoming request. - :param call_next: The callable representing the next middleware or endpoint in the chain. - :return: The response from the middleware or endpoint. - """ - if request.method == "CONNECT": - # Parse the host and port from the request's path - host, port = request.scope.get("path").split(":") - port = int(port) - - print("proxy_tunnel", host, port) - - raise NotImplementedError( - "Using PromptSail as a true proxy is not supported yet" - ) - - # Create a socket connection to the target server - # client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - # await request.send({"type": "http.response.start", "status": 200}) +# @app.middleware("proxy_tunnel") +# async def __call__(request: Request, call_next): +# """ +# Middleware for handling proxy tunnel requests. - # try: - # await request.app.proxy_tunnel(client_socket, host, port) - # except Exception as e: - # print(f"Error during proxy tunnel: {e}") - # finally: - # client_socket.close() - # - # return Response(content=b"", status_code=200) +# :param request: The incoming request. +# :param call_next: The callable representing the next middleware or endpoint in the chain. +# :return: The response from the middleware or endpoint. +# """ +# if request.method == "CONNECT": +# # Parse the host and port from the request's path +# host, port = request.scope.get("path").split(":") +# port = int(port) + +# print("proxy_tunnel", host, port) + +# raise NotImplementedError( +# "Using PromptSail as a true proxy is not supported yet" +# ) + +# # Create a socket connection to the target server +# # client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +# # await request.send({"type": "http.response.start", "status": 200}) + +# # try: +# # await request.app.proxy_tunnel(client_socket, host, port) +# # except Exception as e: +# # print(f"Error during proxy tunnel: {e}") +# # finally: +# # client_socket.close() +# # +# # return Response(content=b"", status_code=200) - response = await call_next(request) - return response +# response = await call_next(request) +# return response diff --git a/backend/src/app/reverse_proxy.py b/backend/src/app/reverse_proxy.py index d2ef8793..8d108975 100644 --- a/backend/src/app/reverse_proxy.py +++ b/backend/src/app/reverse_proxy.py @@ -19,9 +19,15 @@ async def iterate_stream(response, buffer): """ Asynchronously iterate over the raw stream of a response and accumulate chunks in a buffer. - :param response: The response object. - :param buffer: The buffer to accumulate chunks. - :return: An asynchronous generator yielding chunks from the response stream. + This function asynchronously iterate over the raw stream of a response and accumulate chunks in a buffer + for later processing. + + Parameters: + - **response**: The response object containing the stream + - **buffer**: List to store the accumulated response chunks + + Yields: + - Chunks of the response data as they are received """ async for chunk in response.aiter_raw(): buffer.append(chunk) @@ -31,8 +37,8 @@ async def iterate_stream(response, buffer): async def close_stream( app: Application, project_id, - request, - response, + ai_provider_request, + ai_provider_response, buffer, tags, ai_model_version, @@ -40,25 +46,29 @@ async def close_stream( request_time, ): """ - Asynchronously close the response stream and store the transaction in the database. - - :param app: The Application instance. - :param project_id: The Project ID. - :param request: The incoming request. - :param response: The response object. - :param buffer: The buffer containing accumulated chunks. - :param tags: The tags associated with the transaction. - :param ai_model_version: Specific tag for AI model. Helps with cost count. - :param pricelist: The pricelist for the models. - :param request_time: The request time. + Process and store transaction data after stream completion. + + This function handles the post-streaming tasks, including storing transaction details + and raw request/response data in the database. + + Parameters: + - **app**: The Application instance + - **project_id**: The unique identifier of the project + - **ai_provider_request**: The original request object + - **ai_provider_response**: The response object from the AI provider + - **buffer**: Buffer containing the accumulated response data + - **tags**: List of tags associated with the transaction + - **ai_model_version**: Specific model version tag for cost calculation + - **pricelist**: List of provider prices for cost calculation + - **request_time**: Timestamp when the request was initiated """ - await response.aclose() + await ai_provider_response.aclose() with app.transaction_context() as ctx: data = ctx.call( store_transaction, project_id=project_id, - request=request, - response=response, + ai_provider_request=ai_provider_request, + ai_provider_response=ai_provider_response, buffer=buffer, tags=tags, ai_model_version=ai_model_version, @@ -67,9 +77,9 @@ async def close_stream( ) ctx.call( store_raw_transactions, - request=request, + request=ai_provider_request, request_content=data["request_content"], - response=response, + response=ai_provider_response, response_content=data["response_content"], transaction_id=data["transaction_id"], ) @@ -90,17 +100,30 @@ async def reverse_proxy( target_path: str | None = None, ): """ - API route for reverse proxying requests to the upstream server. - - :param project_slug: The slug of the project. - :param provider_slug: The slug of the AI provider. - :param path: The path for the reverse proxy. - :param request: The incoming request. - :param ctx: The transaction context dependency. - :param tags: Optional. Tags associated with the transaction. - :param ai_model_version: Optional. Specific tag for AI model. Helps with cost count. - :param target_path: Optional. Target path for the reverse proxy. - :return: A StreamingResponse object. + Forward requests to AI providers and handle responses. + + This endpoint acts as a reverse proxy, forwarding requests to various AI providers + while monitoring and storing transaction details. It handles streaming responses, + calculates costs, and maintains transaction history. + + Parameters: + - **project_slug**: The unique slug identifier of the project + - **provider_slug**: The slug identifier of the AI provider + - **path**: The API endpoint path to forward to + - **request**: The incoming request object + - **ctx**: The transaction context dependency + - **tags**: Optional comma-separated list of tags for the transaction + - **ai_model_version**: Optional specific model version for accurate cost calculation + - **target_path**: Optional override for the target API path + + Returns: + - A StreamingResponse object containing the provider's response + + Notes: + - Automatically handles request/response streaming + - Stores transaction details and raw data in the background + - Calculates costs based on the provider's pricing + - Supports various HTTP methods (GET, POST, PUT, PATCH, DELETE) """ logger = get_logger(request) @@ -112,6 +135,7 @@ async def reverse_proxy( project = ctx.call(get_project_by_slug, slug=project_slug) url = ApiURLBuilder.build(project, provider_slug, path, target_path) + # todo: remove this, this logic should be in the use case pricelist = get_provider_pricelist(request) logger.debug(f"got projects for {project}") @@ -125,7 +149,7 @@ async def reverse_proxy( timeout = httpx.Timeout(100.0, connect=50.0) request_time = datetime.now(tz=timezone.utc) - rp_req = client.build_request( + ai_provider_request = client.build_request( method=request.method, url=url, headers={ @@ -136,19 +160,19 @@ async def reverse_proxy( timeout=timeout, ) logger.debug(f"Requesting on: {url}") - rp_resp = await client.send(rp_req, stream=True, follow_redirects=True) + ai_provider_response = await client.send(ai_provider_request, stream=True, follow_redirects=True) buffer = [] return StreamingResponse( - iterate_stream(rp_resp, buffer), - status_code=rp_resp.status_code, - headers=rp_resp.headers, + iterate_stream(ai_provider_response, buffer), + status_code=ai_provider_response.status_code, + headers=ai_provider_response.headers, background=BackgroundTask( close_stream, ctx["app"], project.id, - rp_req, - rp_resp, + ai_provider_request, + ai_provider_response, buffer, tags, ai_model_version, diff --git a/backend/src/app/web_api.py b/backend/src/app/web_api.py index b76c2e90..be63c8d6 100644 --- a/backend/src/app/web_api.py +++ b/backend/src/app/web_api.py @@ -2,9 +2,13 @@ from collections import defaultdict from typing import Annotated, Any +import numpy as np +import pandas as pd + + import pandas as pd import utils -from _datetime import datetime, timezone +from _datetime import datetime, timezone, timedelta from app.dependencies import get_provider_pricelist, get_transaction_context from auth.authorization import decode_and_validate_token from auth.models import User @@ -48,6 +52,7 @@ CreateTransactionWithRawDataSchema, GetTagStatisticsInTime, GetTagStatisticsSchema, + GetTransactionLatencyStatisticsSchema, GetTransactionLatencyStatisticsWithoutDateSchema, GetTransactionPageResponseSchema, GetTransactionSchema, @@ -78,6 +83,18 @@ def whoami( request: Request, user: User = Depends(decode_and_validate_token) ) -> GetUserSchema: + """ + Get the current user's information. + + This endpoint returns the authenticated user's details. + + Parameters: + - **request**: The incoming request object. + - **user**: The authenticated user object. + + Returns: + - A GetUserSchema object containing the user's details. + """ return GetUserSchema( external_id=user.external_id, organization=user.organization, @@ -94,10 +111,15 @@ async def get_projects( ctx: Annotated[TransactionContext, Depends(get_transaction_context)], ) -> list[GetProjectSchema]: """ - API endpoint to retrieve information about all projects. + Retrieve all projects. + + This endpoint returns information about all projects, including transaction counts and total costs. - :param ctx: The transaction context dependency. - :return: A list of GetProjectSchema objects. + Parameters: + - **ctx**: The transaction context dependency. + + Returns: + - A list of GetProjectSchema objects representing all projects. """ projects = ctx.call(get_all_projects) @@ -132,11 +154,19 @@ async def get_project_details( ctx: Annotated[TransactionContext, Depends(get_transaction_context)], ) -> GetProjectSchema: """ - API endpoint to retrieve details about a specific project. + Retrieve details of a specific project. + + This endpoint returns detailed information about a specific project, including transaction count and total cost. + + Parameters: + - **project_id**: The unique identifier of the project. + - **ctx**: The transaction context dependency. + + Returns: + - A GetProjectSchema object representing the project details. - :param project_id: The identifier of the project. - :param ctx: The transaction context dependency. - :return: A GetProjectSchema object representing the project details. + Raises: + - HTTPException: 404 error if the project is not found. """ try: project = ctx.call(get_project, project_id=project_id) @@ -169,9 +199,12 @@ async def create_project( """ API endpoint to create a new project. - :param data: The data for creating the project as a CreateProjectSchema object. - :param ctx: The transaction context dependency. - :return: A GetProjectSchema object representing the created project. + Parameters: + - **data**: The data for creating the project as a CreateProjectSchema object. + - **ctx**: The transaction context dependency. + + Returns: + - A GetProjectSchema object representing the created project. """ project_id = generate_uuid() project = Project( @@ -200,12 +233,17 @@ async def update_existing_project( ctx: Annotated[TransactionContext, Depends(get_transaction_context)], ) -> GetProjectSchema: """ - API endpoint to update an existing project. + Update an existing project. + + This endpoint updates the details of an existing project. - :param project_id: The identifier of the project to be updated. - :param data: The data for updating the project as an UpdateProjectSchema object. - :param ctx: The transaction context dependency. - :return: A GetProjectSchema object representing the updated project. + Parameters: + - **project_id**: The unique identifier of the project to be updated. + - **data**: The UpdateProjectSchema object containing the updated project details. + - **ctx**: The transaction context dependency. + + Returns: + - A GetProjectSchema object representing the updated project. """ data = dict(**data.model_dump(exclude_none=True)) if "slug" in data: @@ -229,10 +267,16 @@ async def delete_existing_project( ctx: Annotated[TransactionContext, Depends(get_transaction_context)], ): """ - API endpoint to delete an existing project and its associated transactions. + Delete an existing project and its associated transactions. + + This endpoint deletes a project and all its associated transactions. - :param project_id: The identifier of the project to be deleted. - :param ctx: The transaction context dependency. + Parameters: + - **project_id**: The unique identifier of the project to be deleted. + - **ctx**: The transaction context dependency. + + Returns: + - No content (204 status code) on successful deletion. """ ctx.call(delete_project, project_id=project_id) ctx.call(delete_multiple_transactions, project_id=project_id) @@ -249,10 +293,19 @@ async def get_transaction_details( ctx: Annotated[TransactionContext, Depends(get_transaction_context)], ) -> GetTransactionWithRawDataSchema: """ - API endpoint to retrieve details of a specific transaction. + Retrieve details of a specific transaction. + + This endpoint returns detailed information about a specific transaction, including raw request and response data. + + Parameters: + - **transaction_id**: The unique identifier of the transaction. + - **ctx**: The transaction context dependency. + + Returns: + - A GetTransactionWithRawDataSchema object representing the transaction details. - :param transaction_id: The identifier of the transaction. - :param ctx: The transaction context dependency. + Raises: + - HTTPException: 404 error if the transaction is not found. """ try: transaction = ctx.call(get_transaction, transaction_id=transaction_id) @@ -297,19 +350,25 @@ async def get_paginated_transactions( provider_models: str | None = None, ) -> GetTransactionPageResponseSchema: """ - API endpoint to retrieve a paginated list of transactions based on specified filters. - - :param ctx: The transaction context dependency. - :param page: The page number for pagination. - :param page_size: The number of transactions per page. - :param tags: Optional. List of tags to filter transactions by. - :param date_from: Optional. Start date for filtering transactions. - :param date_to: Optional. End date for filtering transactions. - :param project_id: Optional. Project ID to filter transactions by. - :param sort_field: Optional. Field to sort by. - :param sort_type: Optional. Ordering method (asc or desc). - :param status_codes: Optional. List of status codes for filtering transactions. - :param provider_models: Optional. List of providers and models for filtering transactions. + Retrieve a paginated list of transactions with filtering options. + + This endpoint returns a paginated list of transactions that can be filtered by various criteria + such as date range, tags, status codes, and provider models. + + Parameters: + - **page**: The page number to retrieve (default: 1) + - **page_size**: Number of transactions per page (default: 20) + - **tags**: Optional comma-separated list of tags to filter transactions + - **date_from**: Optional start date for filtering transactions + - **date_to**: Optional end date for filtering transactions + - **project_id**: Optional project ID to filter transactions + - **sort_field**: Optional field name to sort by + - **sort_type**: Optional sort direction ('asc' or 'desc') + - **status_codes**: Optional comma-separated list of status codes + - **provider_models**: Optional comma-separated list of provider.model combinations + + Returns: + - A GetTransactionPageResponseSchema containing the paginated transactions and metadata """ if tags is not None: tags = tags.split(",") @@ -385,37 +444,43 @@ async def get_paginated_transactions( async def get_transaction_usage_statistics_over_time( ctx: Annotated[TransactionContext, Depends(get_transaction_context)], project_id: str, - date_from: datetime | str | None = None, - date_to: datetime | str | None = None, + date_from: str , + date_to: str , period: utils.PeriodEnum = utils.PeriodEnum.day, ) -> list[GetTransactionsUsageStatisticsSchema]: """ - Retrieve transaction usage statistics over a specified time period.\n - - This endpoint fetches transaction data based on the specified project ID, - date range, and period. It then processes the data to generate usage statistics - including total input tokens, total output tokens, cumulative input tokens, cumulative output tokens as well as - total cost calculated based on cumulative values for the best possible representation of costs over time.\n - - :param ctx: The transaction context, providing access to dependencies (automatically applied).\n - :param project_id: The unique identifier of the project.\n - :param date_from: Starting point of the time interval (optional - when empty, then the scope is counted from the - beginning of the project's existence).\n - :param date_to: End point of the time interval (optional - when empty, then the interval is counted up to the - present time).\n - :param period: The time period for grouping statistics - can be year, month, week, day, hour or minute (5 minutes). - (default is "day").\n - :return: A list of GetTransactionUsageStatisticsSchema (provider, model, date, total_input_tokens, - total_output_tokens, input_cumulative_total, output_cumulative_total, total_transactions, total_cost) - representing the usage statistics.\n + Calculate cost and usage metrics for transactions within a given time range. + Only includes successful transactions (status code 200) that occurred between the start and end dates. + The time range is inclusive - transactions exactly on the start or end date/time will be included. + + Parameters: + - **ctx**: The transaction context dependency + - **project_id**: The unique identifier of the project + - **date_from**: Start date (ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + - **date_to**: End date (ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + - **period**: Time period for aggregation (year, month, week, day, hour, or 5minutes) + + Returns: + - A list of GetTransactionsUsageStatisticsSchema objects containing cost and usage statistics + grouped by the specified period + + Raises: + - HTTPException: 400 error if dates are invalid or in wrong format """ - date_from, date_to = utils.check_dates_for_statistics(date_from, date_to) + + + + try: + # Validate date format and convert to datetime + date_from_dt, date_to_dt = utils.validate_date_range(date_from, date_to) + except HTTPException as e: + raise e count = ctx.call( count_transactions, project_id=project_id, - date_from=date_from, - date_to=date_to, + date_from=date_from_dt, + date_to=date_to_dt, status_codes=[200], ) if count == 0: @@ -424,8 +489,8 @@ async def get_transaction_usage_statistics_over_time( transactions = ctx.call( get_list_of_filtered_transactions, project_id=project_id, - date_from=date_from, - date_to=date_to, + date_from=date_from_dt, + date_to=date_to_dt, status_codes=[200], ) transactions = [ @@ -449,7 +514,7 @@ async def get_transaction_usage_statistics_over_time( for transaction in transactions ] stats = utils.token_counter_for_transactions( - transactions, period, date_from, date_to + transactions, period, date_from_dt, date_to_dt ) dates = [] for stat in stats: @@ -509,13 +574,17 @@ async def get_transaction_status_statistics_over_time( :return: A list of GetTransactionStatusStatisticsSchema (date, status_code, total_transactions) representing the status statistics.\n """ - date_from, date_to = utils.check_dates_for_statistics(date_from, date_to) + try: + # Validate date format and convert to datetime + date_from_dt, date_to_dt = utils.validate_date_range(date_from, date_to) + except HTTPException as e: + raise e count = ctx.call( count_transactions, project_id=project_id, - date_from=date_from, - date_to=date_to, + date_from=date_from_dt, + date_to=date_to_dt, ) if count == 0: return [] @@ -523,8 +592,8 @@ async def get_transaction_status_statistics_over_time( transactions = ctx.call( get_list_of_filtered_transactions, project_id=project_id, - date_from=date_from, - date_to=date_to, + date_from=date_from_dt, + date_to=date_to_dt, ) transactions = [ StatisticTransactionSchema( @@ -547,42 +616,50 @@ async def get_transaction_status_statistics_over_time( for transaction in transactions ] stats = utils.status_counter_for_transactions( - transactions, period, date_from, date_to + transactions, period, date_from_dt, date_to_dt ) return stats @app.get( - "/api/statistics/transactions_speed", + "/api/statistics/transactions_speed_old", response_class=JSONResponse, dependencies=[Security(decode_and_validate_token)], ) -async def get_transaction_latency_statistics_over_time( +async def get_transactions_speed_statistics_over_time( ctx: Annotated[TransactionContext, Depends(get_transaction_context)], project_id: str, - date_from: datetime | str | None = None, - date_to: datetime | str | None = None, + date_from: str, + date_to: str, period: utils.PeriodEnum = utils.PeriodEnum.day, ) -> list[GetTransactionsLatencyStatisticsSchema]: """ - Compute mean transactions generation speed and latency statistics over a specified time period.\n - - Endpoint fetches transaction data for the project (project ID) and specified date range (date from, date to). Next, it aggregates the generation speed by the provided granularity (monthly, weekly, daily, hourly or by minutes).\n - - :param ctx: The transaction context, providing access to dependencies (automatically applied).\n - :param project_id: The unique identifier of the project.\n - :param date_from: Starting point of the time interval (optional - when empty, then the scope is counted from the - beginning of the project's existence).\n - :param date_to: End point of the time interval (optional - when empty, then the interval is counted up to the - present time).\n - :param period: The time period for grouping statistics - can be year, month, week, day, hour or minute (5 minutes). - (default is "day").\n - :return: A list of GetTransactionLatencyStatisticsSchema (provider, model, date, mean_latency, tokens_per_second, - total_transactions) representing the generation speed and latency statistics.\n + Calculate average generation speed and latency metrics for transactions within a given time range. + Only includes successful transactions (status code 200) that occurred between the start and end dates. + The time range is inclusive - transactions exactly on the start or end date/time will be included. + + Parameters: + - **ctx**: The transaction context dependency + - **project_id**: The unique identifier of the project + - **date_from**: Start date (ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + - **date_to**: End date (ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + - **period**: Time period for aggregation (year, month, week, day, hour, or 5minutes) + + Returns: + - A list of GetTransactionLatencyStatisticsSchema objects containing latency and speed statistics + grouped by the specified period + + Raises: + - HTTPException: 400 error if dates are invalid or in wrong format """ - date_from, date_to = utils.check_dates_for_statistics(date_from, date_to) + try: + # Validate date format and convert to datetime + date_from, date_to = utils.validate_date_range(date_from, date_to) + except HTTPException as e: + raise e + # Continue with existing logic count = ctx.call( count_transactions, project_id=project_id, @@ -591,10 +668,11 @@ async def get_transaction_latency_statistics_over_time( status_codes=[200], null_generation_speed=False, ) + if count == 0: return [] - transactions = ctx.call( + transactions: list[Transaction] = ctx.call( get_list_of_filtered_transactions, project_id=project_id, date_from=date_from, @@ -602,7 +680,8 @@ async def get_transaction_latency_statistics_over_time( status_codes=[200], null_generation_speed=False, ) - transactions = [ + + transactions: list[StatisticTransactionSchema] = [ StatisticTransactionSchema( project_id=project_id, provider=transaction.provider, @@ -649,6 +728,72 @@ async def get_transaction_latency_statistics_over_time( return new_stats +@app.get( + "/api/statistics/transactions_speed", + response_class=JSONResponse, + dependencies=[Security(decode_and_validate_token)], +) +async def get_transactions_speed_statistics_over_time_refactored( + ctx: Annotated[TransactionContext, Depends(get_transaction_context)], + project_id: str, + date_from: str, + date_to: str, + period: utils.PeriodEnum = utils.PeriodEnum.day, +) -> list[GetTransactionsLatencyStatisticsSchema]: + """ + Calculate average generation speed and latency metrics for transactions within a given time range. + Only includes successful transactions (status code 200) that occurred between the start and end dates. + The time range is inclusive - transactions exactly on the start or end date/time will be included. + + Parameters: + - **ctx**: The transaction context dependency + - **project_id**: The unique identifier of the project + - **date_from**: Start date (ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + - **date_to**: End date (ISO format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + - **period**: Time period for aggregation (year, month, week, day, hour, or 5minutes) + + Returns: + - A list of GetTransactionLatencyStatisticsSchema objects containing latency and speed statistics + grouped by the specified period + + Raises: + - HTTPException: 400 error if dates are invalid or in wrong format + """ + try: + # Validate date format and convert to datetime + date_from_dt, date_to_dt = utils.validate_date_range(date_from, date_to) + except HTTPException as e: + raise e + + # Continue with existing logic + count = ctx.call( + count_transactions, + project_id=project_id, + date_from=date_from_dt, + date_to=date_to_dt, + status_codes=[200], + null_generation_speed=False, + ) + + if count == 0: + return [] + + transactions: list[Transaction] = ctx.call( + get_list_of_filtered_transactions, + project_id=project_id, + date_from=date_from_dt, + date_to=date_to_dt, + status_codes=[200], + null_generation_speed=False, + ) + # Transform and calculate statistics + df = utils.prepare_transaction_dataframe(transactions, date_from_dt, date_to_dt) + stats_df = utils.calculate_speed_statistics(df, utils.pandas_period_from_string(period)) + + # Format response + return utils.format_statistics_response(stats_df) + + @app.get( "/api/portfolio/details", response_class=JSONResponse, @@ -657,6 +802,19 @@ async def get_transaction_latency_statistics_over_time( async def get_portfolio_details( ctx: Annotated[TransactionContext, Depends(get_transaction_context)] ) -> GetPortfolioDetailsSchema: + """ + Retrieve portfolio-wide statistics and details. + + This endpoint provides an overview of all projects in the portfolio, including + aggregated statistics such as total costs, total transactions, and individual + project metrics. + + Parameters: + - **ctx**: The transaction context dependency + + Returns: + - A GetPortfolioDetailsSchema containing portfolio-wide statistics and project details + """ project_count = ctx.call(count_projects) total_cost_per_project, total_transactions_per_project = {}, {} total_cost, total_transactions = 0, 0 @@ -701,6 +859,22 @@ async def get_portfolio_usage_in_time( date_to: datetime | str | None = None, period: utils.PeriodEnum = utils.PeriodEnum.day, ) -> list[GetProjectsUsageInTimeSchema]: + """ + Retrieve portfolio usage statistics over time. + + This endpoint provides detailed statistics about portfolio-wide usage patterns, + including token consumption and costs across all projects over time. + + Parameters: + - **ctx**: The transaction context dependency + - **date_from**: Optional start date (format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + - **date_to**: Optional end date (format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + - **period**: Time period for aggregation (year, month, week, day, hour, or 5minutes) + + Returns: + - A list of GetProjectsUsageInTimeSchema objects containing usage statistics + grouped by the specified period + """ project_count = ctx.call(count_projects) projects_usage_in_time = [] results = {} @@ -832,6 +1006,22 @@ async def get_portfolio_costs_by_tag( date_to: datetime | str | None = None, period: utils.PeriodEnum = utils.PeriodEnum.day, ) -> list[GetTagStatisticsInTime]: + """ + Retrieve cost statistics grouped by tags over time. + + This endpoint calculates and returns cost statistics for transactions, + grouped by their tags and aggregated over the specified time period. + + Parameters: + - **ctx**: The transaction context dependency + - **date_from**: Optional start date (format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + - **date_to**: Optional end date (format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + - **period**: Time period for aggregation (year, month, week, day, hour, or 5minutes) + + Returns: + - A list of GetTagStatisticsInTime objects containing cost statistics by tag + grouped by the specified period + """ date_from, date_to = utils.check_dates_for_statistics(date_from, date_to) count = ctx.call( count_transactions, @@ -875,10 +1065,6 @@ async def get_portfolio_costs_by_tag( total_cost=transaction.total_cost, request_time=transaction.request_time, response_time=transaction.response_time, - hate=transaction.hate, - self_harm=transaction.self_harm, - violence=transaction.violence, - sexual=transaction.sexual, ) ) else: @@ -906,10 +1092,6 @@ async def get_portfolio_costs_by_tag( total_cost=transaction.total_cost, request_time=transaction.request_time, response_time=transaction.response_time, - hate=transaction.hate, - self_harm=transaction.self_harm, - violence=transaction.violence, - sexual=transaction.sexual, ) ) @@ -957,9 +1139,17 @@ async def get_portfolio_costs_by_tag( ) async def fetch_provider_pricelist(request: Request) -> list[GetAIProviderPriceSchema]: """ - API endpoint to retrieve a price list of AI providers. + Retrieve the price list for all AI providers. + + This endpoint returns the current price list for all active AI providers, + including their models and associated costs. - :param request: The incoming request. + Parameters: + - **request**: The incoming request object + + Returns: + - A list of GetAIProviderPriceSchema objects containing pricing information + for each active provider and model """ price_list = [ price for price in get_provider_pricelist(request) if price.is_active is True @@ -974,9 +1164,16 @@ async def fetch_provider_pricelist(request: Request) -> list[GetAIProviderPriceS ) async def get_providers(request: Request) -> list[GetAIProviderSchema]: """ - API endpoint to retrieve a list of AI providers. + Retrieve the list of supported AI providers. + + This endpoint returns information about all supported AI providers, + including their names and API base URLs. - :param request: The incoming request. + Parameters: + - **request**: The incoming request object + + Returns: + - A list of GetAIProviderSchema objects containing provider information """ return [GetAIProviderSchema(**provider) for provider in utils.known_ai_providers] @@ -987,9 +1184,21 @@ async def get_config( ctx: Annotated[TransactionContext, Depends(get_transaction_context)], ) -> dict[str, str | bool]: """ - API endpoint to retrieve the config. + Retrieve application configuration settings. + + This endpoint returns the current application configuration, including + organization name and authentication settings (SSO, Azure, Google). - :param ctx: The transaction context dependency. + Parameters: + - **request**: The incoming request object + - **ctx**: The transaction context dependency + + Returns: + - A dictionary containing configuration settings including: + - organization: Organization name + - authorization: SSO authentication status + - azure_auth: Azure authentication availability + - google_auth: Google authentication availability """ config = request.app.container.config() organization_name = ctx.call(get_organization_name) @@ -1007,6 +1216,20 @@ async def get_users( ctx: Annotated[TransactionContext, Depends(get_transaction_context)], auth_user: User = Depends(decode_and_validate_token), ) -> list[GetPartialUserSchema]: + """ + Retrieve a list of all users. + + This endpoint returns a list of all users, with the authenticated user always + appearing first in the list. Each user's information includes their ID, email, + full name, and profile picture. + + Parameters: + - **ctx**: The transaction context dependency + - **auth_user**: The authenticated user making the request + + Returns: + - A list of GetPartialUserSchema objects containing user information + """ users = ctx.call(get_all_users) idx = next( (i for i, usr in enumerate(users) if usr.external_id == auth_user.external_id), @@ -1037,14 +1260,28 @@ async def get_users( dependencies=[Security(decode_and_validate_token)], ) def create_transaction( - request: Request, + request_object: Request, data: CreateTransactionWithRawDataSchema, ctx: Annotated[TransactionContext, Depends(get_transaction_context)], ) -> GetTransactionSchema: + """ + Create a new transaction. + + This endpoint creates a new transaction based on the provided data. It calculates the cost + of the transaction if not provided, using the price list for the specified model and provider. + + Parameters: + - **request_object**: The incoming fastapirequest object. + - **data**: The data for creating the transaction as a CreateTransactionWithRawDataSchema object. + - **ctx**: The transaction context dependency. + + Returns: + - A GetTransactionWithRawDataSchema object representing the created transaction. + """ if ((data.status_code == 200) and data.model and data.provider) and not ( data.input_cost or data.output_cost or data.total_cost ): - pricelist = get_provider_pricelist(request) + pricelist = get_provider_pricelist(request_object) pricelist = [ item for item in pricelist @@ -1070,10 +1307,11 @@ def create_transaction( if not data.generation_speed: if data.output_tokens is not None and (data.output_tokens > 0): - data.generation_speed = ( - data.output_tokens - / (datetime.now(tz=timezone.utc) - data.request_time).total_seconds() + time_elapsed = data.response_time - data.request_time + data.generation_speed = (data.output_tokens + 0.0) / ( + time_elapsed.total_seconds() + 0.000001 ) + elif data.output_tokens == 0: data.generation_speed = None else: @@ -1084,13 +1322,13 @@ def create_transaction( request_data = CreateRawTransactionSchema( transaction_id=created_transaction.id, type=TransactionTypeEnum.request, - data=data.request, + data=data.request_json, ) response_data = CreateRawTransactionSchema( transaction_id=created_transaction.id, type=TransactionTypeEnum.response, - data=data.response, + data=data.response_json, ) raw_transaction_request = ctx.call(add_raw_transaction, data=request_data) @@ -1114,13 +1352,19 @@ async def mock_transactions( date_to: datetime, ) -> dict[str, Any]: """ - API endpoint to generate a set of mock transactions. Warning! This endpoint is only for testing purposes and will delete all transactions for project-test. + Generate mock transactions for testing purposes. + + This endpoint creates a specified number of mock transactions within a given date range. + Warning: This endpoint is only for testing purposes and will delete existing test transactions in the "project-test" project. - :param count: How many transactions you want to mock. - :param date_from: The start date from which transactions should be added. - :param date_to: The end date till which transactions should be added. - :param ctx: The transaction context dependency. - :return: A dictionary containing the status and message (code and latency). + Parameters: + - **ctx**: The transaction context dependency + - **count**: Number of mock transactions to generate + - **date_from**: Start date for the mock transactions (format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + - **date_to**: End date for the mock transactions (format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) + + Returns: + - A dictionary containing the status code and execution time information """ time_start = datetime.now(tz=timezone.utc) transactions_repo = ctx["transaction_repository"] @@ -1143,14 +1387,20 @@ async def mock_transactions( @app.post( "/api/only_for_purpose/remove_mocked_transactions", response_class=JSONResponse ) -async def mock_transactions( +async def remove_mocked_transactions( ctx: Annotated[TransactionContext, Depends(get_transaction_context)], ) -> dict[str, Any]: """ - API endpoint to remove mocked transactions. Warning! This endpoint is only for testing purposes and will delete all transactions for project-test. + Remove mocked transactions. + + This endpoint removes all mocked transactions for the 'project-test' project. + Warning: This endpoint is only for testing purposes. + + Parameters: + - **ctx**: The transaction context dependency. - :param ctx: The transaction context dependency. - :return: A dictionary containing the status and message. + Returns: + - A dictionary containing the status code and a message confirming the removal of mocked transactions. """ transactions_repo = ctx["transaction_repository"] transactions_repo.delete_cascade(project_id="project-test") diff --git a/backend/src/app/web_home.py b/backend/src/app/web_home.py index f87b080f..e6c7ae16 100644 --- a/backend/src/app/web_home.py +++ b/backend/src/app/web_home.py @@ -7,9 +7,18 @@ @app.get("/healthcheck") async def healthcheck(request: Request): """ - API endpoint to information about the API's health status. + Check the API's health status. - :param request: The incoming request. + This endpoint provides basic health check information about the API, + including its current status and timestamp. + + Parameters: + - **request**: The incoming request object + + Returns: + - A dictionary containing: + - status: Current API status ("OK") + - datetime: Current timestamp in UTC """ return {"status": "OK", "datetime": datetime.now(tz=timezone.utc)} @@ -20,9 +29,17 @@ async def healthcheck(request: Request): ) async def home_page(request: Request): """ - API route for the all unsupported traffic. + Handle unsupported traffic routes. + + This endpoint serves as a catch-all route for any requests that don't match + other defined endpoints. It returns a basic response indicating the API is functioning. + + Parameters: + - **request**: The incoming request object - :param request: The incoming request. - :return: A dictionary containing the status and current datetime. + Returns: + - A dictionary containing: + - status: Current API status ("OK") + - datetime: Current timestamp in UTC """ return {"status": "OK", "datetime": datetime.now(tz=timezone.utc)} diff --git a/backend/src/config/__init__.py b/backend/src/config/__init__.py index 4637a9b7..879d1e93 100644 --- a/backend/src/config/__init__.py +++ b/backend/src/config/__init__.py @@ -20,6 +20,7 @@ class Config(BaseSettings): GOOGLE_CLIENT_ID: str | None = os.getenv("GOOGLE_CLIENT_ID", None) AZURE_CLIENT_ID: str | None = os.getenv("AZURE_CLIENT_ID", None) SSO_AUTH: bool = os.getenv("SSO_AUTH", "False").lower() in ("true", "1", "t") + PRICE_LIST_PATH: str = str((BASE_DIR / Path("config/data/provider_price_list.json")).resolve()) config = Config() diff --git a/backend/src/config/containers.py b/backend/src/config/containers.py index 4fc20020..286c9e47 100644 --- a/backend/src/config/containers.py +++ b/backend/src/config/containers.py @@ -7,6 +7,7 @@ from uuid import UUID import pymongo +from pymongo.database import Database as MongoDatabase from app.logging import logger, logging_context from auth.repositories import UserRepository from dependency_injector import containers, providers @@ -213,8 +214,9 @@ def on_exit_transaction_context(ctx: TransactionContext, exception): :param ctx: The TransactionContext. :param exception: The exception (if any) that occurred during the transaction. """ - logger.debug(f"transaction ended ") + logger.debug(f"transaction ended {exception}") logging_context.correlation_id = None + # @application.transaction_middleware # def null_middleware(ctx: TransactionContext, call_next): @@ -246,7 +248,12 @@ class TopLevelContainer(containers.DeclarativeContainer): logger=logger, container=__self__, ) - provider_pricelist = providers.Singleton(read_provider_pricelist) + + # todo: remove this, this logic should be in the use case + provider_pricelist = providers.Singleton( + lambda config: read_provider_pricelist(config.PRICE_LIST_PATH), + config=config + ) class TransactionContainer(containers.DeclarativeContainer): @@ -258,7 +265,7 @@ class TransactionContainer(containers.DeclarativeContainer): correlation_id = providers.Dependency(instance_of=UUID) logger = providers.Dependency(instance_of=Logger) - db_client = providers.Dependency(instance_of=pymongo.database.Database) + db_client = providers.Dependency(instance_of=MongoDatabase) app = providers.Dependency(instance_of=Application) project_repository = providers.Singleton( @@ -278,3 +285,5 @@ class TransactionContainer(containers.DeclarativeContainer): user_repository = providers.Singleton( UserRepository, db_client=db_client, collection_name="users" ) + + #todo: move price calculation provider here diff --git a/backend/provider_price_list.json b/backend/src/config/data/provider_price_list.json similarity index 100% rename from backend/provider_price_list.json rename to backend/src/config/data/provider_price_list.json diff --git a/backend/src/transactions/models.py b/backend/src/transactions/models.py index 4bdc0fe8..3c358990 100644 --- a/backend/src/transactions/models.py +++ b/backend/src/transactions/models.py @@ -33,7 +33,9 @@ class Transaction(BaseModel): response_time: datetime = Field( default_factory=lambda: datetime.now(tz=timezone.utc) ) - hate: Any = None - self_harm: Any = None - violence: Any = None - sexual: Any = None + + # old fields + # hate: Any = None + # self_harm: Any = None + # violence: Any = None + # sexual: Any = None diff --git a/backend/src/transactions/schemas.py b/backend/src/transactions/schemas.py index bb179ed0..8981729a 100644 --- a/backend/src/transactions/schemas.py +++ b/backend/src/transactions/schemas.py @@ -38,6 +38,58 @@ class GetTransactionWithRawDataSchema(GetTransactionWithProjectSlugSchema): response: dict[str, Any] + +class CreateTransactionWithRawDataSchema(BaseModel): + project_id: str + request_json: dict[str, Any] + response_json: dict[str, Any] + tags: list[str] + provider: str + model: str | None + type: str + os: str | None + input_tokens: int | None + output_tokens: int | None + library: str + status_code: int + messages: list[dict[str, Any]] | str | None + last_message: str | None + prompt: str + error_message: str | None + generation_speed: int | float | None + request_time: datetime + input_cost: int | float | None + output_cost: int | float | None + total_cost: int | float | None + response_time: datetime | None + + +class CreateTransactionSchema(BaseModel): + project_id: str + tags: list[str] + provider: str + model: str | None + type: str + os: str | None + input_tokens: int | None + output_tokens: int | None + library: str + status_code: int + messages: list[dict[str, Any]] | str | None + last_message: str | None + prompt: str + error_message: str | None + generation_speed: int | float | None + request_time: datetime + input_cost: int | float | None + output_cost: int | float | None + total_cost: int | float | None + response_time: datetime | None + + + + + class StatisticTransactionSchema(BaseModel): project_id: str provider: str @@ -157,50 +209,3 @@ class GetTransactionPageResponseSchema(BaseModel): total_pages: int total_elements: int - -class CreateTransactionWithRawDataSchema(BaseModel): - project_id: str - request: dict[str, Any] - response: dict[str, Any] - tags: list[str] - provider: str - model: str | None - type: str - os: str | None - input_tokens: int | None - output_tokens: int | None - library: str - status_code: int - messages: list[dict[str, Any]] | str | None - last_message: str | None - prompt: str - error_message: str | None - generation_speed: int | float | None - request_time: datetime - input_cost: int | float | None - output_cost: int | float | None - total_cost: int | float | None - response_time: datetime | None - - -class CreateTransactionSchema(BaseModel): - project_id: str - tags: list[str] - provider: str - model: str | None - type: str - os: str | None - input_tokens: int | None - output_tokens: int | None - library: str - status_code: int - messages: list[dict[str, Any]] | str | None - last_message: str | None - prompt: str - error_message: str | None - generation_speed: int | float | None - request_time: datetime - input_cost: int | float | None - output_cost: int | float | None - total_cost: int | float | None - response_time: datetime | None diff --git a/backend/src/transactions/use_cases.py b/backend/src/transactions/use_cases.py index 42ce992e..b9e867dd 100644 --- a/backend/src/transactions/use_cases.py +++ b/backend/src/transactions/use_cases.py @@ -176,8 +176,8 @@ def delete_multiple_transactions( def store_transaction( - request, - response, + ai_provider_request, + ai_provider_response, buffer, project_id, tags, @@ -189,8 +189,8 @@ def store_transaction( """ Store a transaction in the repository based on request, response, and additional information. - :param request: The request object. - :param response: The response object. + :param ai_provider_request: The request object. + :param ai_provider_response: The response object. :param buffer: The buffer containing the response content. :param project_id: The Project ID associated with the transaction. :param tags: The tags associated with the transaction. @@ -200,17 +200,18 @@ def store_transaction( :param transaction_repository: An instance of TransactionRepository used for storing transaction data. :return: None """ - response_content = utils.preprocess_buffer(request, response, buffer) + + response_content = utils.preprocess_buffer(ai_provider_request, ai_provider_response, buffer) param_extractor = utils.TransactionParamExtractor( - request, response, response_content + ai_provider_request, ai_provider_response, response_content ) params = param_extractor.extract() ai_model_version = ( ai_model_version if ai_model_version is not None else params["model"] ) - + pricelist = [ item for item in pricelist @@ -262,7 +263,7 @@ def store_transaction( generation_speed = 0 try: - content = json.loads(request.content) + content = json.loads(ai_provider_request.content) except UnicodeDecodeError: content = param_extractor.request_content @@ -332,11 +333,11 @@ def get_list_of_filtered_transactions( ) transactions = transaction_repository.get_filtered(query) return transactions - - def add_transaction( data: CreateTransactionSchema, transaction_repository: TransactionRepository ) -> Transaction: transaction = Transaction(**data.model_dump()) transaction_repository.add(transaction) return transaction + + diff --git a/backend/src/utils.py b/backend/src/utils.py index 24f78d2b..61f74419 100644 --- a/backend/src/utils.py +++ b/backend/src/utils.py @@ -22,8 +22,22 @@ GetTransactionUsageStatisticsSchema, StatisticTransactionSchema, TagStatisticTransactionSchema, + GetTransactionLatencyStatisticsWithoutDateSchema, + GetTransactionPageResponseSchema, + GetTransactionSchema, + GetTransactionsLatencyStatisticsSchema, + GetTransactionStatusStatisticsSchema, + GetTransactionsUsageStatisticsSchema, + GetTransactionUsageStatisticsWithoutDateSchema, + GetTransactionWithProjectSlugSchema, + GetTransactionWithRawDataSchema, + StatisticTransactionSchema, + TagStatisticTransactionSchema, ) +from fastapi import HTTPException +from datetime import datetime, timezone + def serialize_data(obj): obj["_id"] = obj["id"] @@ -1424,6 +1438,7 @@ def status_counter_for_transactions( return result_list +# speed statistics utils functions def speed_counter_for_transactions( transactions: list[StatisticTransactionSchema], @@ -1533,7 +1548,150 @@ def speed_counter_for_transactions( return result_list +def prepare_transaction_dataframe( + transactions: list[Transaction], + date_from: datetime | None = None, + date_to: datetime | None = None, +) -> pd.DataFrame: + """ + Convert transactions to DataFrame and add boundary dates with zero values. + + Args: + transactions: List of transactions + date_from: Start date for analysis + date_to: End date for analysis + + Returns: + DataFrame with transactions and boundary dates + """ + if not transactions: + return pd.DataFrame() + + # Convert transactions to DataFrame efficiently + df = pd.DataFrame([ + { + "project_id": tx.project_id, + "provider": tx.provider, + "model": tx.model, + "total_input_tokens": tx.input_tokens or 0, + "total_output_tokens": tx.output_tokens or 0, + "total_input_cost": tx.input_cost or 0, + "total_output_cost": tx.output_cost or 0, + "total_cost": tx.total_cost or 0, + "date": tx.response_time, + "latency": (tx.response_time - tx.request_time).total_seconds(), + "generation_speed": tx.generation_speed or 0, + "total_transactions": 1, + } + for tx in transactions + ]) + + # Add boundary dates if specified + if date_from or date_to: + boundary_records = _create_boundary_records( + df, date_from, date_to + ) + if not boundary_records.empty: + df = pd.concat([df, boundary_records], ignore_index=True) + + return df + +def _create_boundary_records( + df: pd.DataFrame, + date_from: datetime | None, + date_to: datetime | None +) -> pd.DataFrame: + """Create records for boundary dates with zero values.""" + if df.empty: + return pd.DataFrame() + + # Get unique provider/model pairs + pairs = df[["provider", "model"]].drop_duplicates() + project_id = df["project_id"].iloc[0] + + records = [] + for date in filter(None, [date_from, date_to]): + # Create zero-value records for each provider/model pair + zero_records = pairs.assign( + date=pd.Timestamp(date), + project_id=project_id, + total_input_tokens=0, + total_output_tokens=0, + total_input_cost=0, + total_output_cost=0, + total_cost=0, + # Set to NaN for values that should not affect mean calculations + latency=np.nan, + generation_speed=np.nan, + ) + records.append(zero_records) + + return pd.concat(records) if records else pd.DataFrame() + +def calculate_speed_statistics( + df: pd.DataFrame, + period: str, +) -> pd.DataFrame: + """ + Calculate speed and latency statistics using pandas operations. + + Args: + df: DataFrame with transaction data + period: Time period for aggregation + + Returns: + DataFrame with aggregated statistics + """ + if df.empty: + return pd.DataFrame() + + # Set date as index for resampling + df = df.set_index('date') + + # Group by provider/model and resample by period + grouped = df.groupby(['provider', 'model']).resample(period).agg({ + 'latency': 'mean', # Direct mean calculation + 'generation_speed': 'mean', # Direct mean calculation + 'total_transactions': 'sum' + }).reset_index() + + # Clean up the results + grouped = grouped.fillna(0) + + return grouped + +def format_statistics_response( + df: pd.DataFrame +) -> list[GetTransactionsLatencyStatisticsSchema]: + """Convert DataFrame to API response format.""" + if df.empty: + return [] + + # Group by date first + date_groups = df.groupby('date') + + # Convert to final format + return [ + GetTransactionsLatencyStatisticsSchema( + date=date, + records=[ + GetTransactionLatencyStatisticsWithoutDateSchema( + provider=row.provider, + model=row.model, + mean_latency=row.latency, + tokens_per_second=row.generation_speed, + total_transactions=row.total_transactions, + ) + for _, row in group.iterrows() + ] + ) + for date, group in date_groups + ] + + + +# def token_counter_for_transactions_by_tag( transactions: list[TagStatisticTransactionSchema], period: str, @@ -1857,127 +2015,10 @@ def generate_mock_transactions(n: int, date_from: datetime, date_to: datetime): return transactions -def check_dates_for_statistics( - date_from: datetime | str | None, date_to: datetime | str | None -) -> tuple[datetime | None, datetime | None]: - if isinstance(date_from, str): - if len(date_from) == 10: - date_from = datetime.fromisoformat(str(date_from) + "T00:00:00") - elif date_from.endswith("Z"): - date_from = datetime.fromisoformat(str(date_from)[:-1]) - else: - date_from = datetime.fromisoformat(date_from) - if isinstance(date_to, str): - if len(date_to) == 10: - date_to = datetime.fromisoformat(date_to + "T23:59:59") - elif date_to.endswith("Z"): - date_to = datetime.fromisoformat(str(date_to)[:-1]) - else: - date_to = datetime.fromisoformat(date_to) - if date_from is not None and date_to is not None and date_from == date_to: - date_to = date_to + timedelta(days=1) - timedelta(seconds=1) - return date_from, date_to -def read_transactions_from_csv( - path: str = "../test_transactions.csv", -) -> list[Transaction]: - df = pd.read_csv(path, sep=";") - data = df.to_dict(orient="records") - transactions = [] - pricelist = read_provider_pricelist() - for idx, obj in enumerate(data): - transaction_id = f"test-transaction-{idx}" - request_time = datetime.fromisoformat( - obj["request_time"].replace("Z", "+00:00") - ) - response_time = datetime.fromisoformat( - obj["response_time"].replace("Z", "+00:00") - ) - latency = response_time - request_time - price = [ - item - for item in pricelist - if item.provider == obj["provider"] - and re.match(item.match_pattern, obj["model"]) - ] - if obj["status_code"] == 200: - if len(price) > 0: - price = price[0] - if price.input_price == 0: - input_cost, output_cost = 0, 0 - total_cost = ( - (obj["input_tokens"] + obj["output_tokens"]) - / 1000 - * price.total_price - ) - else: - input_cost = price.input_price * (obj["input_tokens"] / 1000) - output_cost = price.output_price * (obj["output_tokens"] / 1000) - total_cost = input_cost + output_cost - else: - input_cost, output_cost, total_cost = None, None, None - transactions.append( - Transaction( - id=transaction_id, - project_id="project-test", - request={}, - response={}, - tags=["tag"], - provider=obj["provider"], - model=obj["model"], - type="chat", - os=None, - input_tokens=obj["input_tokens"], - output_tokens=obj["output_tokens"], - library="PostmanRuntime/7.36.3", - status_code=obj["status_code"], - messages=None, - prompt="", - last_message="", - error_message=None, - request_time=request_time, - response_time=response_time, - generation_speed=obj["output_tokens"] / latency.total_seconds() - if latency.total_seconds() > 0 - else 0, - input_cost=input_cost, - output_cost=output_cost, - total_cost=total_cost, - ) - ) - else: - transactions.append( - Transaction( - id=transaction_id, - project_id="project-test", - request={}, - response={}, - tags=["tag"], - provider=obj["provider"], - model=obj["model"], - type="chat", - os=None, - input_tokens=0, - output_tokens=0, - library="PostmanRuntime/7.36.3", - status_code=obj["status_code"], - messages=None, - prompt="", - last_message="", - error_message="Error", - request_time=request_time, - response_time=response_time, - generation_speed=0, - input_cost=0, - output_cost=0, - total_cost=0, - ) - ) - return transactions - def count_tokens_for_streaming_response(messages: list | str, model: str) -> int: encoder = tiktoken.encoding_for_model(model) @@ -2000,13 +2041,7 @@ def json(self): return self.content -def truncate_float(number, decimals): - if isinstance(number, float): - str_number = str(number) - integer_part, decimal_part = str_number.split(".") - truncated_decimal_part = decimal_part[:decimals] - return float(f"{integer_part}.{truncated_decimal_part}") - return number + def resize_b64_image(b64_image: str | str, new_size: tuple[int, int]) -> str: @@ -2143,3 +2178,90 @@ class PeriodEnum(str, Enum): "api_base_placeholder": "https://llmapi.provider.com/v1", }, ] + +def validate_date_range(date_from: datetime | str, date_to: datetime | str) -> tuple[datetime, datetime]: + """ + Validate and convert date strings to datetime objects, ensuring they form a valid date range. + If time zone is not specified, it is assumed to be UTC. + If time zone is specified, it is converted to UTC. + + + Parameters: + - date_from: Start date (datetime object or string in YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS format) + - date_to: End date (datetime object or string in YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS format) + + Returns: + - Tuple of validated datetime objects (date_from, date_to) + + Raises: + - HTTPException: 400 error if dates are invalid or in wrong format + """ + + # initialize dates as datetime objects, ih they are strings they will be converted later + date_from_obj = date_from + date_to_obj = date_to + + try: + # Check if date_from and date_to are in ISO format with optional timezone offset or Z for UTC + if isinstance(date_from_obj, str): + # replace the space before the timezone offset with a + + date_from_obj = date_from_obj.replace(" ", "+") + if not re.match(r'^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}([+-]\d{2}:?\d{2}|Z)?)?$', date_from_obj): + raise ValueError("Invalid date format") + date_from_obj = datetime.fromisoformat(date_from_obj) + + if isinstance(date_to_obj, str): + # replace the space before the timezone offset with a + + date_to_obj = date_to_obj.replace(" ", "+") + if not re.match(r'^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}([+-]\d{2}:?\d{2}|Z)?)?$', date_to_obj): + raise ValueError("Invalid date format") + date_to_obj = datetime.fromisoformat(date_to_obj) + + except ValueError as e: + raise HTTPException( + status_code=400, + detail=f"Invalid date format. Use YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS: {e}" + ) + + + # Validate date range + if date_from_obj > date_to_obj: + raise HTTPException( + status_code=400, + detail="date_from cannot be after date_to" + ) + + # we don't validate dates are not in the future, because we want to allow to get statistics for future dates + + # convert to UTC if date_from or date_to are in UTC + date_from_obj = date_from_obj.astimezone(timezone.utc) if date_from_obj.tzinfo else date_from_obj + date_to_obj = date_to_obj.astimezone(timezone.utc) if date_to_obj.tzinfo else date_to_obj + + # remove the timezone offset for already converted dates to utc + date_from_obj = date_from_obj.replace(tzinfo=None) + date_to_obj = date_to_obj.replace(tzinfo=None) + + return date_from_obj, date_to_obj + +def check_dates_for_statistics( + date_from: datetime | str | None, date_to: datetime | str | None +) -> tuple[datetime | None, datetime | None]: + if isinstance(date_from, str): + if len(date_from) == 10: + date_from = datetime.fromisoformat(str(date_from) + "T00:00:00") + elif date_from.endswith("Z"): + date_from = datetime.fromisoformat(str(date_from)[:-1]) + else: + date_from = datetime.fromisoformat(date_from) + if isinstance(date_to, str): + if len(date_to) == 10: + date_to = datetime.fromisoformat(date_to + "T23:59:59") + elif date_to.endswith("Z"): + date_to = datetime.fromisoformat(str(date_to)[:-1]) + else: + date_to = datetime.fromisoformat(date_to) + + if date_from is not None and date_to is not None and date_from == date_to: + date_to = date_to + timedelta(days=1) - timedelta(seconds=1) + + return date_from, date_to diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 2395f76e..cc694f72 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,5 +1,8 @@ import pytest from fastapi.testclient import TestClient +from datetime import datetime, timezone +from projects.models import Project, AIProvider +from test_utils import read_transactions_from_csv @pytest.fixture @@ -32,3 +35,61 @@ def application(fastapi_instance): # ctx["transaction_repository"].remove_all() return app + + +@pytest.fixture +def test_project(application): + """Create a default test project for API tests.""" + with application.transaction_context() as ctx: + project = Project( + id="project-test", + name="Autotest", + slug="autotest1", + description="Project 1 description", + ai_providers=[ + AIProvider( + deployment_name="openai", + slug="openai", + api_base="https://api.openai.com/v1", + description="New test project", + provider_name="provider1", + ) + ], + tags=["test", "api-test"], + org_id="test-organization", + owner="test@example.com" + ) + + repo = ctx["project_repository"] + repo.add(project) + + yield project + + # Use delete instead of delete_cascade + repo.delete(project.id) + + +@pytest.fixture +def test_project_id(test_project): + """Helper fixture to get just the project ID""" + return test_project.id + + +@pytest.fixture +def test_transactions_count(application): + """Fixture to load and store test transactions""" + with application.transaction_context() as ctx: + repo = ctx["transaction_repository"] + transactions = read_transactions_from_csv("test_transactions.csv") + for transaction in transactions: + repo.add(transaction) + yield transactions + repo.remove_all() + + +@pytest.fixture(autouse=True) +def clean_database(application): + """Automatically clean database before each test""" + for collection in application["db_client"].list_collection_names(): + application["db_client"][collection].drop() + yield diff --git a/backend/test_transactions.csv b/backend/tests/fixtures/transactions/test_transactions.csv similarity index 100% rename from backend/test_transactions.csv rename to backend/tests/fixtures/transactions/test_transactions.csv diff --git a/backend/test_transactions_pricing.csv b/backend/tests/fixtures/transactions/test_transactions_pricing.csv similarity index 100% rename from backend/test_transactions_pricing.csv rename to backend/tests/fixtures/transactions/test_transactions_pricing.csv diff --git a/backend/test_transactions_tokens_cost_speed.csv b/backend/tests/fixtures/transactions/test_transactions_tokens_cost_speed.csv similarity index 100% rename from backend/test_transactions_tokens_cost_speed.csv rename to backend/tests/fixtures/transactions/test_transactions_tokens_cost_speed.csv diff --git a/backend/tests/test_api transactions.py b/backend/tests/test_api transactions.py new file mode 100644 index 00000000..65831e95 --- /dev/null +++ b/backend/tests/test_api transactions.py @@ -0,0 +1,708 @@ +from datetime import datetime, timezone, timedelta +from transactions.schemas import CreateTransactionWithRawDataSchema +from projects.schemas import GetProjectSchema +import pytest + + +header = { + "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyMTAyODc3OTUzNDg0MzUyNDI3IiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiYm54OW9WT1o4U3FJOTcyczBHYjd4dyIsIm5hbWUiOiJUZXN0IFVzZXIiLCJwaWN0dXJlIjoiIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJpYXQiOjE3MTM3MzQ0NjEsImV4cCI6OTk5OTk5OTk5OX0.eZYMQzcSRzkAq4Me8C6SNU3wduS7EIu_o5XGAbsDmU05GtyipQEb5iNJ1QiLg-11RbZFL3dvi8xKd3mpuw8b-5l6u8hwSpZg6wNPLY0zPX-EOwxeHLtev_2X5pUf1_IWAnso9K_knsK8CcmJoVsCyNNjlw3hrkChacJHGNzg0TTT1rh3oe6KCpbLvYlV6tUPfm5k3AMFZIT7Jntr38CZvs6gac6L_DhItJc3TNNUUHie2zgA29_r9YFlaEr_nGoSmBhIi-i0i0h34TL4JAb4qJkVM2YI2eTTv2HjEGtkx4mE5JvNQ0VxzHSJcCNOHh1gCiFD5c6rhvvxVeEqMkGGbCZKHX_vCgnIp0iE_OWyICjVTFPitQJ00fXLhyHyPb7q5J605tuK2iTHp2NCRJEXIAl9e0F_qASBBAfyL0C4FCBtvbnEMwtpoV1VWinkKgkI7JVH0AsyTugjXyAjxxsJxBTJT9qwZLxVBoaxgqNTOFfxvwstyq1VfCl3iBbpt71D" +} + +# Add this with other test data at the top of the file +test_transaction = { + "project_id": "project-test", # This will match our test project ID + "request_json": { + "method": "POST", + "url": "https://api.openai.com/v1/chat/completions?tags=dev,test&target_path=/chat/completions", + "host": "api.openai.com", + "headers": { + "host": "api.openai.com", + "connection": "close", + "content-length": "304", + "accept": "application/json", + "user-agent": "OpenAI/Python 1.35.7", + "accept-encoding": "gzip, deflate", + "authorization": "Bearer sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "max-forwards": "10", + "x-stainless-lang": "python", + "x-stainless-package-version": "1.35.7", + "x-stainless-os": "Windows", + "x-stainless-arch": "other:amd64", + "x-stainless-runtime": "CPython", + "x-stainless-runtime-version": "3.10.11", + "x-stainless-async": "false", + "x-arr-log-id": "4e939686-fa43-4b14-b1c8-1316b943d77c", + "client-ip": "79.184.224.10:64769", + "x-client-ip": "79.184.224.10", + "disguised-host": "try-promptsail.azurewebsites.net", + "x-site-deployment-id": "try-promptsail", + "was-default-hostname": "try-promptsail.azurewebsites.net", + "x-forwarded-proto": "https", + "x-appservice-proto": "https", + "x-arr-ssl": "2048|256|CN=Microsoft Azure RSA TLS Issuing CA 03, O=Microsoft Corporation, C=US|CN=*.azurewebsites.net, O=Microsoft Corporation, L=Redmond, S=WA, C=US", + "x-forwarded-tlsversion": "1.3", + "x-forwarded-for": "79.184.224.10:64769", + "x-original-url": "/api/models-playground/openai/?tags=dev_2,test&target_path=/chat/completions", + "x-waws-unencoded-url": "/api/models-playground/openai/?tags=dev_2,test&target_path=/chat/completions", + "x-client-port": "64769", + "content-type": "application/json", + }, + "extensions": { + "timeout": {"connect": 50, "read": 100, "write": 100, "pool": 100} + }, + "content": { + "messages": [ + { + "role": "system", + "content": "You are an experienced marketer, who helps companies to create their marketing strategy.", + }, + { + "role": "user", + "content": "Hello", + }, + ], + "model": "gpt-3.5-turbo-0125", + "temperature": 0.5, + }, + }, + "response_json": { + "status_code": 200, + "headers": { + "date": "Thu, 24 Oct 2024 20:37:47 GMT", + "content-type": "application/json", + "transfer-encoding": "chunked", + "connection": "close", + "access-control-expose-headers": "X-Request-ID", + "openai-organization": "ermlab-software", + "openai-processing-ms": "5752", + "openai-version": "2020-10-01", + "x-ratelimit-limit-requests": "10000", + "x-ratelimit-limit-tokens": "200000", + "x-ratelimit-remaining-requests": "9999", + "x-ratelimit-remaining-tokens": "199938", + "x-ratelimit-reset-requests": "8.64s", + "x-ratelimit-reset-tokens": "18ms", + "x-request-id": "req_34c80008a9bdf45008eaf3a0b1b1eb8a", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "cf-cache-status": "DYNAMIC", + "set-cookie": "__cf_bm=63oSPzfIgrh2XHvHIhPy._mzS8FaUBoY_aoLtBOYkKs-1729802267-1.0.1.1-Lpkpb1G3CkL1avTcO_HamD3uTj5A54g0RpJcaIalmf7dJPAlldf_tN9qOSRy6fegziJAqGgPNiPw7C4joCQrEg; path=/; expires=Thu, 24-Oct-24 21:07:47 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None, _cfuvid=gF0UbGQ5.6CmFcpYCz.9AFVWz2DAZtiUnFJ8ppSe2Dw-1729802267379-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None", + "x-content-type-options": "nosniff", + "server": "cloudflare", + "cf-ray": "8d7cc4247ee30ead-AMS", + "content-encoding": "gzip", + "alt-svc": 'h3=":443"; ma=86400', + }, + "is_error": "false", + "is_success": "true", + "content": { + "id": "chatcmpl-ALysP7bzYZUx3U2TgcTFSTiooLBt4", + "object": "chat.completion", + "created": 1729802261, + "model": "gpt-3.5-turbo-0125", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hi there!", + }, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 40, + "completion_tokens": 25, + "total_tokens": 65, + "prompt_tokens_details": {"cached_tokens": 0}, + "completion_tokens_details": {"reasoning_tokens": 0}, + }, + "system_fingerprint": None, + }, + "elapsed": 6.253704, + "encoding": "utf-8", + }, + "tags": ["test-tag"], + "provider": "OpenAI", + "model": "gpt-3.5-turbo-0125", + "type": "chat", + "os": None, + "input_tokens": 10, + "output_tokens": 20, + "library": "python-openai", + "status_code": 200, + "messages": [ + { + "role": "system", + "content": "You are an experienced marketer, who helps companies to create their marketing strategy.", + }, + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + ], + "last_message": "Hi there!", + "prompt": "Hello", + "error_message": None, + "generation_speed": None, + # Convert datetime strings to ISO format + "request_time": datetime.now(tz=timezone.utc).isoformat(), + "input_cost": None, + "output_cost": None, + "total_cost": None, + "response_time": datetime.now(tz=timezone.utc).isoformat(), +} + + +# ------------------------------ +# Create transactions tests +# ------------------------------ + +def test_create_transaction_with_valid_data_returns_201_and_calculated_costs(client, application, test_project): + """Test successful transaction creation with cost calculation""" + # arrange + data = CreateTransactionWithRawDataSchema( + **{ + **test_transaction, + # Convert ISO strings back to datetime objects for the schema + "request_time": datetime.fromisoformat(test_transaction["request_time"]), + "response_time": datetime.fromisoformat(test_transaction["response_time"]), + } + ) + + # Convert to JSON-serializable format for the request + json_data = data.model_dump(mode='json') + + # act + response = client.post("/api/transactions", headers=header, json=json_data) + + # assert + assert response.status_code == 201 + transaction = response.json() + assert transaction["project_id"] == data.project_id + assert transaction["provider"] == data.provider + assert transaction["model"] == data.model + assert transaction["input_tokens"] == data.input_tokens + assert transaction["output_tokens"] == data.output_tokens + assert transaction["status_code"] == data.status_code + assert transaction["total_cost"] is not None # Cost should be calculated + assert transaction["input_cost"] is not None + assert transaction["output_cost"] is not None + + # Verify cost calculation + assert transaction["input_cost"] > 0 + assert transaction["output_cost"] > 0 + assert transaction["total_cost"] == transaction["input_cost"] + transaction["output_cost"] + +def test_create_transaction_with_failed_response_returns_201_and_error_details(client, application, test_project): + """Test transaction creation with failed API response""" + # arrange + data = test_transaction.copy() + data["status_code"] = 400 + data["error_message"] = "Bad request" + data["response_json"] = {"error": {"message": "Bad request"}} + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + assert transaction["status_code"] == 400 + assert transaction["error_message"] == "Bad request" + assert transaction["input_cost"] is None + assert transaction["output_cost"] is None + assert transaction["total_cost"] is None + +def test_create_transaction_with_image_generation_model_returns_201_and_proper_costs(client, application, test_project): + """Test transaction creation for image generation""" + # arrange + data = test_transaction.copy() + data["model"] = "standard/1024x1024/dall-e-3" + data["type"] = "image_generation" + data["request_json"]["n"] = 1 # Number of images + + # act + response = client.post("/api/transactions", headers=header, json=data) + + pytest.skip("todo: add cost calculation") + # assert + assert response.status_code == 201 + transaction = response.json() + assert transaction["type"] == "image_generation" + + # todo: add cost calculation + #assert transaction["total_cost"] == pytest.approx(0.040, rel=1e-4) + assert transaction["input_cost"] == 0 # Image generation has no input cost + +def test_create_transaction_with_missing_required_fields_returns_422(client, application): + """Test transaction creation with missing required fields""" + # arrange + invalid_data = { + "project_id": "project-test", + # Missing required fields + } + + # act + response = client.post("/api/transactions", headers=header, json=invalid_data) + + # assert + assert response.status_code == 422 # Validation error + +def test_create_transaction_with_unknown_model_returns_201_and_null_costs(client, application, test_project): + """Test transaction creation with unknown model""" + # arrange + data = test_transaction.copy() + data["model"] = "unknown-model" + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + assert transaction["model"] == "unknown-model" + assert transaction["input_cost"] is None + assert transaction["output_cost"] is None + assert transaction["total_cost"] is None + +# -------------------------------- +# Transaction cost calculation +# -------------------------------- + +def test_transaction_costs_with_embedding_model_returns_201_and_correct_costs(client, application, test_project): + """Test if transaction costs are calculated accurately for embedding model""" + # arrange + data = test_transaction.copy() + data.update({ + "model": "text-embedding-3-large", + "type": "embedding", + "input_tokens": 256, # Known input tokens + "output_tokens": 0, # Embedding models don't have output tokens + # Clear any existing cost values to test calculation + "input_cost": None, + "output_cost": None, + "total_cost": None + }) + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + + # text-embedding-3-large pricing: $0.00013/1K tokens (input only) + expected_input_cost = 0 # No input cost for embeddings + expected_output_cost = 0 # No output cost for embeddings + expected_total_cost = (data["input_tokens"] / 1000) * 0.00013 # $0.00003328 # Total cost equals input cost + + # Check if calculated costs match expected values (using approximate comparison due to floating point) + assert transaction["input_cost"] == pytest.approx(expected_input_cost, rel=1e-4) + assert transaction["output_cost"] == pytest.approx(expected_output_cost, rel=1e-4) + assert transaction["total_cost"] == pytest.approx(expected_total_cost, rel=1e-4) + + # Additional embedding-specific assertions + assert transaction["type"] == "embedding" + assert transaction["output_tokens"] == 0 + +def test_transaction_costs_with_embedding_model_with_none_output_tokens_returns_201_and_correct_costs(client, application, test_project): + """Test if transaction costs are calculated accurately for embedding model with None output tokens""" + # arrange + data = test_transaction.copy() + data.update({ + "model": "text-embedding-3-large", + "type": "embedding", + "input_tokens": 256, # Known input tokens + "output_tokens": None, # Embedding models don't have output tokens + # Clear any existing cost values to test calculation + "input_cost": None, + "output_cost": None, + "total_cost": None + }) + + pytest.skip("todo: needs adding the output_tokens none verification") + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + + # Additional embedding-specific assertions + assert transaction["type"] == "embedding" + assert transaction["output_tokens"] is None + + # text-embedding-3-large pricing: $0.00013/1K tokens (input only) + expected_input_cost = 0 # No input cost for embeddings + expected_output_cost = 0 # No output cost for embeddings + expected_total_cost = (data["input_tokens"] / 1000) * 0.00013 # $0.00003328 # Total cost equals input cost + + # Check if calculated costs match expected values (using approximate comparison due to floating point) + assert transaction["input_cost"] is None + assert transaction["output_cost"] is None + assert transaction["total_cost"] == pytest.approx(expected_total_cost, rel=1e-4) + +def test_transaction_costs_with_embedding_model_with_set_output_tokens_returns_201_and_correct_costs(client, application, test_project): + """Test if transaction costs are calculated accurately for embedding model with set output tokens""" + # arrange + data = test_transaction.copy() + data.update({ + "model": "text-embedding-3-large", + "type": "embedding", + "input_tokens": 256, # Known input tokens + "output_tokens": 100, # Embedding models should have output tokens, this should not be taken into account for cost calculation + # Clear any existing cost values to test calculation + "input_cost": None, + "output_cost": None, + "total_cost": None + }) + + pytest.skip("todo: needs adding the output_tokens none verification") + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + + # Additional embedding-specific assertions + assert transaction["type"] == "embedding" + assert transaction["output_tokens"] is None + + # text-embedding-3-large pricing: $0.00013/1K tokens (input only) + expected_input_cost = 0 # No input cost for embeddings + expected_output_cost = 0 # No output cost for embeddings + expected_total_cost = (data["input_tokens"] / 1000) * 0.00013 # $0.00003328 # Total cost equals input cost + + # Check if calculated costs match expected values (using approximate comparison due to floating point) + assert transaction["input_cost"] is None + assert transaction["output_cost"] is None + assert transaction["total_cost"] == pytest.approx(expected_total_cost, rel=1e-4) + + +def test_transaction_costs_with_chat_model_returns_201_and_correct_costs(client, application, test_project): + """Test if transaction costs are calculated accurately based on token counts""" + # arrange + data = test_transaction.copy() + data.update({ + "model": "gpt-3.5-turbo-0125", # Specific model with known pricing + "input_tokens": 100, # Known input tokens + "output_tokens": 50, # Known output tokens + # Clear any existing cost values to test calculation + "input_cost": None, + "output_cost": None, + "total_cost": None + }) + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + + # GPT-3.5-turbo-0125 pricing: $0.0005/1K input tokens, $0.0015/1K output tokens + expected_input_cost = (data["input_tokens"] / 1000) * 0.0005 # $0.00005 + expected_output_cost = (data["output_tokens"] / 1000) * 0.0015 # $0.000075 + expected_total_cost = expected_input_cost + expected_output_cost # $0.000125 + + # Check if calculated costs match expected values (using approximate comparison due to floating point) + assert transaction["input_cost"] == pytest.approx(expected_input_cost, rel=1e-4) + assert transaction["output_cost"] == pytest.approx(expected_output_cost, rel=1e-4) + assert transaction["total_cost"] == pytest.approx(expected_total_cost, rel=1e-4) + + # Verify the relationship between costs + assert transaction["total_cost"] == transaction["input_cost"] + transaction["output_cost"] + + + +def test_transaction_costs_with_precalculated_values_returns_201_and_same_costs(client, application, test_project): + """Test transaction creation with pre-calculated costs""" + # arrange + data = test_transaction.copy() + data["input_cost"] = 0.001 + data["output_cost"] = 0.002 + data["total_cost"] = 0.003 + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + assert transaction["input_cost"] == data["input_cost"] + assert transaction["output_cost"] == data["output_cost"] + assert transaction["total_cost"] == data["total_cost"] + +def test_transaction_costs_with_unknown_model_returns_201_and_null_costs(client, application, test_project): + """Test if transaction costs are handled properly for unknown/unsupported models""" + # arrange + data = test_transaction.copy() + data.update({ + "model": "gpt-5-future-model", # Non-existent model + "type": "chat", + "input_tokens": 100, + "output_tokens": 50, + # Clear any existing cost values to test calculation + "input_cost": None, + "output_cost": None, + "total_cost": None + }) + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + + # For unknown models, all costs should be None + assert transaction["input_cost"] is None + assert transaction["output_cost"] is None + assert transaction["total_cost"] is None + + # Other transaction data should still be recorded correctly + assert transaction["model"] == "gpt-5-future-model" + assert transaction["input_tokens"] == 100 + assert transaction["output_tokens"] == 50 + assert transaction["type"] == "chat" + +def test_transaction_costs_with_two_image_generation_returns_201_and_correct_costs(client, application, test_project): + """Test if transaction costs are calculated accurately for DALL-E 3 1024x1792 image generation""" + # arrange + data = test_transaction.copy() + data.update({ + "model": "standard/1024x1792/dall-e-3", # DALL-E 3 with specific size + "type": "image_generation", + "input_tokens": 0, # Image generation doesn't use tokens + "output_tokens": 0, # Image generation doesn't use tokens + "request_json": { + "n": 2, # Number of images requested + "size": "1024x1792", # Image size + "quality": "standard", # Image quality + "model": "dall-e-3" + }, + # Clear any existing cost values to test calculation + "input_cost": None, + "output_cost": None, + "total_cost": None + }) + + pytest.skip("todo: add cost calculation for image generation models") + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + + # DALL-E 3 1024x1792 pricing: $0.080 per image (standard quality) + cost_per_image = 0.080 + expected_total_cost = cost_per_image * data["request_json"]["n"] # $0.160 for 2 images + + + + # Additional image generation specific assertions + assert transaction["type"] == "image_generation" + assert transaction["model"] == "standard/1024x1792/dall-e-3" + assert transaction["input_tokens"] == 0 + assert transaction["output_tokens"] == 0 + + # Check if calculated costs match expected values + assert transaction["input_cost"] == 0 # Image generation has no input cost + assert transaction["output_cost"] == 0 # Image generation has no output cost + assert transaction["total_cost"] == pytest.approx(expected_total_cost, rel=1e-4) + + + +# -------------------------------- +# Transation generation speed tests +# -------------------------------- + +def test_transaction_generation_speed_with_chat_model_returns_201_and_calculated_speed(client, application, test_project): + """Test generation speed calculation for chat model with output tokens""" + # arrange + data = test_transaction.copy() + + request_time = datetime.fromisoformat("2024-11-03T12:00:00") + response_time = request_time + timedelta(seconds=2.5) + + data.update({ + "model": "gpt-3.5-turbo-0125", + "type": "chat", + "input_tokens": 100, + "output_tokens": 50, + "generation_speed": None, # Clear generation speed to test calculation + "request_time": request_time.isoformat(), + "response_time": response_time.isoformat() + }) + + + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + + # Generation speed should be output_tokens / time_elapsed + assert transaction["generation_speed"] is not None + assert transaction["generation_speed"] > 0 + + expected_speed = (data["output_tokens"] / (datetime.fromisoformat(data["response_time"]) - datetime.fromisoformat(data["request_time"])).total_seconds()) + + + assert expected_speed == pytest.approx(transaction["generation_speed"], rel=0.01) + +def test_transaction_generation_speed_with_image_model_returns_201_and_null_speed(client, application, test_project): + """Test generation speed calculation for image generation model (zero output tokens)""" + # arrange + data = test_transaction.copy() + data.update({ + "model": "standard/1024x1024/dall-e-3", + "type": "image_generation", + "input_tokens": 0, + "output_tokens": 0, # Image generation has no output tokens + "generation_speed": None, + "request_time": (datetime.now(tz=timezone.utc) - timedelta(seconds=3.2)).isoformat(), + "response_time": datetime.now(tz=timezone.utc).isoformat() + }) + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + + # Generation speed should be None for zero output tokens + assert transaction["generation_speed"] is None + +def test_transaction_generation_speed_with_embedding_model_returns_201_and_zero_speed(client, application, test_project): + """Test generation speed calculation for embedding model (None output tokens)""" + # arrange + data = test_transaction.copy() + + # set date to 2024-11-03T12:00:00 + request_time = datetime.fromisoformat("2024-11-03T12:00:00") + response_time = request_time + timedelta(seconds=2.5) + + data.update({ + "model": "text-embedding-3-large", + "type": "embedding", + "input_tokens": 100, + "output_tokens": None, # Embedding models don't have output tokens + "generation_speed": None, + "request_time": request_time.isoformat(), + "response_time": response_time.isoformat() + }) + + pytest.skip("todo: needs adding the output_tokens none verification") + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + + # Generation speed should be 0 for None output tokens + assert transaction["generation_speed"] == 0 + + + +def test_transaction_generation_speed_with_existing_value_returns_201_and_unchanged_speed(client, application, test_project): + """Test that existing generation speed is not overwritten""" + # arrange + data = test_transaction.copy() + + request_time = datetime.fromisoformat("2024-11-03T12:00:00") + response_time = request_time + timedelta(seconds=2) + data.update({ + "model": "gpt-3.5-turbo-0125", + "type": "chat", + "input_tokens": 100, + "output_tokens": 50, + "generation_speed": 15.5, # Pre-set generation speed + "request_time": request_time.isoformat(), + "response_time": response_time.isoformat() + }) + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + + # Generation speed should remain unchanged + assert transaction["generation_speed"] == 15.5 + +def test_transaction_generation_speed_with_fast_response_returns_201_and_calculated_speed(client, application, test_project): + """Test generation speed calculation with very small time difference""" + # arrange + data = test_transaction.copy() + request_time = datetime.now(tz=timezone.utc) + response_time = request_time + timedelta(milliseconds=10) + data.update({ + "model": "gpt-3.5-turbo-0125", + "type": "chat", + "input_tokens": 100, + "output_tokens": 500, + "generation_speed": None, + # Set request_time very close to current time + "request_time": request_time.isoformat(), + "response_time": response_time.isoformat() + }) + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + + # Generation speed should be calculated even with very small time difference + assert transaction["generation_speed"] > 0 + + expected_speed = (data["output_tokens"] / (datetime.fromisoformat(data["response_time"]) - datetime.fromisoformat(data["request_time"])).total_seconds()) + + + assert expected_speed == pytest.approx(transaction["generation_speed"], rel=0.01) + + + +def test_transaction_generation_speed_with_future_request_time_returns_201_and_null_speed(client, application, test_project): + """Test generation speed calculation with future request time (edge case)""" + # arrange + data = test_transaction.copy() + request_time = datetime.now(tz=timezone.utc) + timedelta(seconds=20) + response_time = datetime.now(tz=timezone.utc) + data.update({ + "model": "gpt-3.5-turbo-0125", + "type": "chat", + "input_tokens": 100, + "output_tokens": 50, + "generation_speed": None, + # Set request_time in the future (edge case) + "request_time": request_time.isoformat(), + "response_time": response_time.isoformat() + }) + + pytest.skip("todo: needs adding check for future request time") + + # act + response = client.post("/api/transactions", headers=header, json=data) + + # assert + assert response.status_code == 201 + transaction = response.json() + + # Generation speed should still be calculated + assert transaction["generation_speed"] is None + + + + diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py deleted file mode 100644 index 0c06a8ec..00000000 --- a/backend/tests/test_api.py +++ /dev/null @@ -1,166 +0,0 @@ -test_obj = { - "name": "Autotest", - "slug": "autotest1", - "description": "Project 1 description", - "ai_providers": [ - { - "deployment_name": "openai", - "slug": "openai", - "api_base": "https://api.openai.com/v1", - "description": "New test project", - "provider_name": "provider1", - } - ], - "tags": ["tag1", "tag2"], - "org_id": "organization", - "owner": "owner", -} - -header = { - "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyMTAyODc3OTUzNDg0MzUyNDI3IiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiYm54OW9WT1o4U3FJOTcyczBHYjd4dyIsIm5hbWUiOiJUZXN0IFVzZXIiLCJwaWN0dXJlIjoiIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJpYXQiOjE3MTM3MzQ0NjEsImV4cCI6OTk5OTk5OTk5OX0.eZYMQzcSRzkAq4Me8C6SNU3wduS7EIu_o5XGAbsDmU05GtyipQEb5iNJ1QiLg-11RbZFL3dvi8xKd3mpuw8b-5l6u8hwSpZg6wNPLY0zPX-EOwxeHLtev_2X5pUf1_IWAnso9K_knsK8CcmJoVsCyNNjlw3hrkChacJHGNzg0TTT1rh3oe6KCpbLvYlV6tUPfm5k3AMFZIT7Jntr38CZvs6gac6L_DhItJc3TNNUUHie2zgA29_r9YFlaEr_nGoSmBhIi-i0i0h34TL4JAb4qJkVM2YI2eTTv2HjEGtkx4mE5JvNQ0VxzHSJcCNOHh1gCiFD5c6rhvvxVeEqMkGGbCZKHX_vCgnIp0iE_OWyICjVTFPitQJ00fXLhyHyPb7q5J605tuK2iTHp2NCRJEXIAl9e0F_qASBBAfyL0C4FCBtvbnEMwtpoV1VWinkKgkI7JVH0AsyTugjXyAjxxsJxBTJT9qwZLxVBoaxgqNTOFfxvwstyq1VfCl3iBbpt71D" -} - - -def test_create_project_returns_201(client, application): - # arrange - ... - - # act - response = client.post("/api/projects", headers=header, json=test_obj) - - # assert - assert response.status_code == 201 - assert_obj = test_obj.copy() - assert_obj["id"] = response.json()["id"] - assert_obj["total_transactions"] = response.json()["total_transactions"] - assert_obj["total_cost"] = response.json()["total_cost"] - assert_obj["created_at"] = response.json()["created_at"] - assert response.json() == assert_obj - - -def test_create_project_with_existing_slug_returns_400(client, application): - # arrange - with application.transaction_context() as ctx: - from projects.models import Project - - repo = ctx["project_repository"] - repo.add(Project(**test_obj)) - - # act - response = client.post("/api/projects", headers=header, json=test_obj) - - # assert - assert response.status_code == 400 - assert response.json() == {"message": f'Slug already exists: {test_obj["slug"]}'} - - -def test_when_no_projects_returns_200_and_empy_list(client, application): - # arrange - ... - - # act - result = client.get("/api/projects", headers=header) - - # assert - assert result.status_code == 200 - assert result.json() == [] - - -def test_get_project_happy_path(client, application): - # arrange - with application.transaction_context() as ctx: - from projects.models import Project - - repo = ctx["project_repository"] - repo.add(Project(id="project1", **test_obj)) - project_id = repo.find_one({"slug": test_obj["slug"]}).id - - # act - response = client.get(f"/api/projects/{project_id}", headers=header) - - # assert - assert response.status_code == 200 - assert response.json() == dict( - id="project1", - total_transactions=0, - total_cost=0, - created_at=response.json()["created_at"], - **test_obj, - ) - - -def test_update_project(client, application): - # arrange - with application.transaction_context() as ctx: - from projects.models import Project - - repo = ctx["project_repository"] - project_id = "project1" - repo.add(Project(id=project_id, **test_obj)) - - # act - response = client.put( - f"/api/projects/{project_id}", headers=header, json={"name": "Autotest2"} - ) - - # assert - assert response.status_code == 200 - assert response.json() == test_obj | dict( - id=project_id, - total_transactions=0, - total_cost=0, - name="Autotest2", - created_at=response.json()["created_at"], - ) - - -def test_delete_project(client, application): - # arrange - with application.transaction_context() as ctx: - from projects.models import Project - - repo = ctx["project_repository"] - project_id = "project-test" - repo.add(Project(id=project_id, **test_obj)) - - # act - response = client.delete(f"/api/projects/{project_id}", headers=header) - - # assert - assert response.status_code == 204 - - -def test_delete_not_existing_project_returns_204(client, application): - # arrange - ... - - # act - response = client.delete(f"/api/projects/non-existing", headers=header) - - # assert - assert response.status_code == 204 - - -def test_get_projects(client, application): - # arrange - with application.transaction_context() as ctx: - from projects.models import Project - - repo = ctx["project_repository"] - project_id = "project-test" - repo.add(Project(id=project_id, **test_obj)) - - # act - result = client.get("/api/projects", headers=header) - - # assert - assert result.status_code == 200 - assert result.json() == [ - dict( - id=project_id, - total_transactions=0, - total_cost=0, - created_at=result.json()[0]["created_at"], - **test_obj, - ) - ] diff --git a/backend/tests/test_api_portfolio.py b/backend/tests/test_api_portfolio.py new file mode 100644 index 00000000..7af5641a --- /dev/null +++ b/backend/tests/test_api_portfolio.py @@ -0,0 +1,142 @@ +from datetime import datetime +import pytest +from test_utils import read_transactions_with_prices_from_csv + + + + +class TestBasePortfolio: + @pytest.fixture(autouse=True) + def setup(self, client, application): + self.client = client + self.application = application + + self.header = { + "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyMTAyODc3OTUzNDg0MzUyNDI3IiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiYm54OW9WT1o4U3FJOTcyczBHYjd4dyIsIm5hbWUiOiJUZXN0IFVzZXIiLCJwaWN0dXJlIjoiIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJpYXQiOjE3MTM3MzQ0NjEsImV4cCI6OTk5OTk5OTk5OX0.eZYMQzcSRzkAq4Me8C6SNU3wduS7EIu_o5XGAbsDmU05GtyipQEb5iNJ1QiLg-11RbZFL3dvi8xKd3mpuw8b-5l6u8hwSpZg6wNPLY0zPX-EOwxeHLtev_2X5pUf1_IWAnso9K_knsK8CcmJoVsCyNNjlw3hrkChacJHGNzg0TTT1rh3oe6KCpbLvYlV6tUPfm5k3AMFZIT7Jntr38CZvs6gac6L_DhItJc3TNNUUHie2zgA29_r9YFlaEr_nGoSmBhIi-i0i0h34TL4JAb4qJkVM2YI2eTTv2HjEGtkx4mE5JvNQ0VxzHSJcCNOHh1gCiFD5c6rhvvxVeEqMkGGbCZKHX_vCgnIp0iE_OWyICjVTFPitQJ00fXLhyHyPb7q5J605tuK2iTHp2NCRJEXIAl9e0F_qASBBAfyL0C4FCBtvbnEMwtpoV1VWinkKgkI7JVH0AsyTugjXyAjxxsJxBTJT9qwZLxVBoaxgqNTOFfxvwstyq1VfCl3iBbpt71D" + } + + + with self.application.transaction_context() as ctx: + repo = ctx["transaction_repository"] + repo.delete_cascade(project_id="project-test") + + config = self.application['config'] + + transactions = read_transactions_with_prices_from_csv( + "test_transactions_tokens_cost_speed.csv", + config.PRICE_LIST_PATH + ) + for transaction in transactions: + repo.add(transaction) + + yield + + # Clean up test data + # with self.application.transaction_context() as ctx: + # repo = ctx["transaction_repository"] + # repo.delete_cascade(project_id="project-test") + + + + +class TestPortfolioCostsByTagAPIContract(TestBasePortfolio): + """Tests for API contract in portfolio costs by tag statistics. Check for errors and invalid parameters and contract for returned data.""" + def make_costs_by_tag_request(self, period, date_from, date_to): + """Helper method to make API request for portfolio statistics.""" + + if date_from is None: + date_from = "" + if date_to is None: + date_to = "" + + api_url = f"/api/portfolio/costs_by_tag?" + #create api url add parameters if they are not empty + + if period != "": + api_url += f"period={period}" + if date_from != "": + api_url += f"&date_from={date_from}" + if date_to != "": + api_url += f"&date_to={date_to}" + + + return self.client.get( + api_url, + headers=self.header, + ) + + def test_costs_by_tag_check_contract(self): + """Tests costs by tag. Check the contract fields for returned data.""" + + + + date_from = "2023-11-01T00:00:00" + date_to = "2023-11-02T00:00:00" + + response = self.make_costs_by_tag_request( + "day", + date_from, + date_to + ) + + resp_data = response.json() + + expected_dates = [ datetime.fromisoformat(date_from), + datetime.fromisoformat(date_to), + ] + + response_dates = [ datetime.fromisoformat(resp_data[0]['date']), + datetime.fromisoformat(resp_data[1]['date']) + ] + + assert response.status_code == 200 + + assert len(resp_data) == 2 + assert response_dates == expected_dates + assert len(resp_data[0]['records']) == 2 + assert len(resp_data[1]['records']) == 2 + + records_0 = resp_data[0]['records'] + records_1 = resp_data[1]['records'] + + # check the contract of the first record, if all fields are present and have correct types: tag, total_input_tokens, total_output_tokens, input_cumulative_total, output_cumulative_total, total_cost, total_transactions + + assert records_0[0]['tag'] == 'tag-0' + assert records_0[0]['total_input_tokens'] == 1143 + assert records_0[0]['total_output_tokens'] == 6474 + assert records_0[0]['input_cumulative_total'] == 1143 + assert records_0[0]['output_cumulative_total'] == 6474 + assert records_0[0]['total_cost'] == pytest.approx(0.0313251, rel=1e-4) + assert records_0[0]['total_transactions'] == 14 + + # check the contract of the second record + assert records_0[1]['tag'] == 'tag-1' + assert records_0[1]['total_input_tokens'] == 1220 + assert records_0[1]['total_output_tokens'] == 4379 + assert records_0[1]['input_cumulative_total'] == 1220 + assert records_0[1]['output_cumulative_total'] == 4379 + assert records_0[1]['total_cost'] == pytest.approx(0.0483646, rel=1e-4) + assert records_0[1]['total_transactions'] == 15 + + + + #check the contract of the second date + assert records_1[0]['tag'] == 'tag-0' + assert records_1[0]['total_input_tokens'] == 0 + assert records_1[0]['total_output_tokens'] == 0 + assert records_1[0]['input_cumulative_total'] == 1143 + assert records_1[0]['output_cumulative_total'] == 6474 + assert records_1[0]['total_cost'] == pytest.approx(0, rel=1e-4) + assert records_1[0]['total_transactions'] == 0 + + assert records_1[1]['tag'] == 'tag-1' + assert records_1[1]['total_input_tokens'] == 0 + assert records_1[1]['total_output_tokens'] == 0 + assert records_1[1]['input_cumulative_total'] == 1220 + assert records_1[1]['output_cumulative_total'] == 4379 + assert records_1[1]['total_cost'] == pytest.approx(0, rel=1e-4) + assert records_1[1]['total_transactions'] == 0 + + + + diff --git a/backend/tests/test_api_projects.py b/backend/tests/test_api_projects.py new file mode 100644 index 00000000..9c635dcc --- /dev/null +++ b/backend/tests/test_api_projects.py @@ -0,0 +1,414 @@ +from datetime import datetime, timezone, timedelta +from transactions.schemas import CreateTransactionWithRawDataSchema +from projects.schemas import GetProjectSchema +import pytest + + +header = { + "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyMTAyODc3OTUzNDg0MzUyNDI3IiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiYm54OW9WT1o4U3FJOTcyczBHYjd4dyIsIm5hbWUiOiJUZXN0IFVzZXIiLCJwaWN0dXJlIjoiIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJpYXQiOjE3MTM3MzQ0NjEsImV4cCI6OTk5OTk5OTk5OX0.eZYMQzcSRzkAq4Me8C6SNU3wduS7EIu_o5XGAbsDmU05GtyipQEb5iNJ1QiLg-11RbZFL3dvi8xKd3mpuw8b-5l6u8hwSpZg6wNPLY0zPX-EOwxeHLtev_2X5pUf1_IWAnso9K_knsK8CcmJoVsCyNNjlw3hrkChacJHGNzg0TTT1rh3oe6KCpbLvYlV6tUPfm5k3AMFZIT7Jntr38CZvs6gac6L_DhItJc3TNNUUHie2zgA29_r9YFlaEr_nGoSmBhIi-i0i0h34TL4JAb4qJkVM2YI2eTTv2HjEGtkx4mE5JvNQ0VxzHSJcCNOHh1gCiFD5c6rhvvxVeEqMkGGbCZKHX_vCgnIp0iE_OWyICjVTFPitQJ00fXLhyHyPb7q5J605tuK2iTHp2NCRJEXIAl9e0F_qASBBAfyL0C4FCBtvbnEMwtpoV1VWinkKgkI7JVH0AsyTugjXyAjxxsJxBTJT9qwZLxVBoaxgqNTOFfxvwstyq1VfCl3iBbpt71D" +} + +# Add this with other test data at the top of the file +test_transaction = { + "project_id": "project-test", # This will match our test project ID + "request_json": { + "method": "POST", + "url": "https://api.openai.com/v1/chat/completions?tags=dev,test&target_path=/chat/completions", + "host": "api.openai.com", + "headers": { + "host": "api.openai.com", + "connection": "close", + "content-length": "304", + "accept": "application/json", + "user-agent": "OpenAI/Python 1.35.7", + "accept-encoding": "gzip, deflate", + "authorization": "Bearer sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "max-forwards": "10", + "x-stainless-lang": "python", + "x-stainless-package-version": "1.35.7", + "x-stainless-os": "Windows", + "x-stainless-arch": "other:amd64", + "x-stainless-runtime": "CPython", + "x-stainless-runtime-version": "3.10.11", + "x-stainless-async": "false", + "x-arr-log-id": "4e939686-fa43-4b14-b1c8-1316b943d77c", + "client-ip": "79.184.224.10:64769", + "x-client-ip": "79.184.224.10", + "disguised-host": "try-promptsail.azurewebsites.net", + "x-site-deployment-id": "try-promptsail", + "was-default-hostname": "try-promptsail.azurewebsites.net", + "x-forwarded-proto": "https", + "x-appservice-proto": "https", + "x-arr-ssl": "2048|256|CN=Microsoft Azure RSA TLS Issuing CA 03, O=Microsoft Corporation, C=US|CN=*.azurewebsites.net, O=Microsoft Corporation, L=Redmond, S=WA, C=US", + "x-forwarded-tlsversion": "1.3", + "x-forwarded-for": "79.184.224.10:64769", + "x-original-url": "/api/models-playground/openai/?tags=dev_2,test&target_path=/chat/completions", + "x-waws-unencoded-url": "/api/models-playground/openai/?tags=dev_2,test&target_path=/chat/completions", + "x-client-port": "64769", + "content-type": "application/json", + }, + "extensions": { + "timeout": {"connect": 50, "read": 100, "write": 100, "pool": 100} + }, + "content": { + "messages": [ + { + "role": "system", + "content": "You are an experienced marketer, who helps companies to create their marketing strategy.", + }, + { + "role": "user", + "content": "Hello", + }, + ], + "model": "gpt-3.5-turbo-0125", + "temperature": 0.5, + }, + }, + "response_json": { + "status_code": 200, + "headers": { + "date": "Thu, 24 Oct 2024 20:37:47 GMT", + "content-type": "application/json", + "transfer-encoding": "chunked", + "connection": "close", + "access-control-expose-headers": "X-Request-ID", + "openai-organization": "ermlab-software", + "openai-processing-ms": "5752", + "openai-version": "2020-10-01", + "x-ratelimit-limit-requests": "10000", + "x-ratelimit-limit-tokens": "200000", + "x-ratelimit-remaining-requests": "9999", + "x-ratelimit-remaining-tokens": "199938", + "x-ratelimit-reset-requests": "8.64s", + "x-ratelimit-reset-tokens": "18ms", + "x-request-id": "req_34c80008a9bdf45008eaf3a0b1b1eb8a", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "cf-cache-status": "DYNAMIC", + "set-cookie": "__cf_bm=63oSPzfIgrh2XHvHIhPy._mzS8FaUBoY_aoLtBOYkKs-1729802267-1.0.1.1-Lpkpb1G3CkL1avTcO_HamD3uTj5A54g0RpJcaIalmf7dJPAlldf_tN9qOSRy6fegziJAqGgPNiPw7C4joCQrEg; path=/; expires=Thu, 24-Oct-24 21:07:47 GMT; domain=.api.openai.com; HttpOnly; Secure; SameSite=None, _cfuvid=gF0UbGQ5.6CmFcpYCz.9AFVWz2DAZtiUnFJ8ppSe2Dw-1729802267379-0.0.1.1-604800000; path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None", + "x-content-type-options": "nosniff", + "server": "cloudflare", + "cf-ray": "8d7cc4247ee30ead-AMS", + "content-encoding": "gzip", + "alt-svc": 'h3=":443"; ma=86400', + }, + "is_error": "false", + "is_success": "true", + "content": { + "id": "chatcmpl-ALysP7bzYZUx3U2TgcTFSTiooLBt4", + "object": "chat.completion", + "created": 1729802261, + "model": "gpt-3.5-turbo-0125", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Hi there!", + }, + "finish_reason": "stop", + } + ], + "usage": { + "prompt_tokens": 40, + "completion_tokens": 25, + "total_tokens": 65, + "prompt_tokens_details": {"cached_tokens": 0}, + "completion_tokens_details": {"reasoning_tokens": 0}, + }, + "system_fingerprint": None, + }, + "elapsed": 6.253704, + "encoding": "utf-8", + }, + "tags": ["test-tag"], + "provider": "OpenAI", + "model": "gpt-3.5-turbo-0125", + "type": "chat", + "os": None, + "input_tokens": 10, + "output_tokens": 20, + "library": "python-openai", + "status_code": 200, + "messages": [ + { + "role": "system", + "content": "You are an experienced marketer, who helps companies to create their marketing strategy.", + }, + {"role": "user", "content": "Hello"}, + {"role": "assistant", "content": "Hi there!"}, + ], + "last_message": "Hi there!", + "prompt": "Hello", + "error_message": None, + "generation_speed": None, + # Convert datetime strings to ISO format + "request_time": datetime.now(tz=timezone.utc).isoformat(), + "input_cost": None, + "output_cost": None, + "total_cost": None, + "response_time": datetime.now(tz=timezone.utc).isoformat(), +} + + + +# -------------------------------- +# Projects CRUD tests +# -------------------------------- + +def test_create_project_with_valid_data_returns_201(client, application): + """Test creating a new project with valid data returns 201 and correct project data""" + # arrange + new_project_data = { + "name": "Test Marketing Campaign", + "slug": "test-marketing-2024", + "description": "Project for testing AI-driven marketing campaign optimization", + "ai_providers": [ + { + "deployment_name": "marketing-gpt", + "slug": "marketing-gpt", + "api_base": "https://api.openai.com/v1", + "description": "GPT model optimized for marketing content", + "provider_name": "OpenAI Marketing", + } + ], + "tags": ["marketing", "test", "gpt-optimization"], + "org_id": "test-marketing-org", + "owner": "test.marketer@example.com", + } + + # act + response = client.post("/api/projects", headers=header, json=new_project_data) + + # assert + assert response.status_code == 201 + + expected_response = new_project_data.copy() + expected_response.update({ + "id": response.json()["id"], + "total_transactions": 0, + "total_cost": 0, + "created_at": response.json()["created_at"] + }) + + assert response.json() == expected_response + +def test_create_project_with_existing_slug_returns_400_and_error(client, application, test_project): + """Test creating a project with an existing slug returns 400 and error message""" + # arrange - test_project fixture has already created a project with slug "autotest1" + # Create a new project data without datetime fields + new_project_data = { + "name": test_project.name, + "slug": test_project.slug, # Same slug as test_project + "description": test_project.description, + "ai_providers": [provider.model_dump() for provider in test_project.ai_providers], + "tags": test_project.tags, + "org_id": test_project.org_id, + "owner": test_project.owner + } + + # act - try to create another project with the same slug + response = client.post("/api/projects", headers=header, json=new_project_data) + + # assert + assert response.status_code == 400 + assert response.json() == {"message": f'Slug already exists: {test_project.slug}'} + +def test_get_projects_with_empty_database_returns_200_and_empty_list(client, application): + """Test when no projects exist, returns 200 and empty list""" + # arrange + ... + + # act + result = client.get("/api/projects", headers=header) + + # assert + assert result.status_code == 200 + assert result.json() == [] + +def test_get_project_with_valid_id_returns_200_and_project_data(client, application, test_project): + """Test retrieving a project by ID returns 200 and correct project data""" + # act + response = client.get(f"/api/projects/{test_project.id}", headers=header) + response_data = response.json() + + # assert + assert response.status_code == 200 + + # Prepare expected data + expected_response = { + "id": test_project.id, + "name": test_project.name, + "slug": test_project.slug, + "description": test_project.description, + "ai_providers": [provider.model_dump() for provider in test_project.ai_providers], + "tags": test_project.tags, + "org_id": test_project.org_id, + "owner": test_project.owner, + "total_transactions": 0, + "total_cost": 0, + "created_at": test_project.created_at.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] # Match API format + } + + # Compare datetime strings after formatting + assert response_data["created_at"].startswith(expected_response["created_at"]) + + # Remove created_at for the rest of the comparison + del response_data["created_at"] + del expected_response["created_at"] + + assert response_data == expected_response + +def test_update_project_with_new_name_returns_200_and_updated_data(client, application, test_project): + """Test updating project name returns 200 and updated project data""" + # arrange + updated_name = "AI Marketing Campaign 2024" + update_data = {"name": updated_name} + + # act + response = client.put( + f"/api/projects/{test_project.id}", + headers=header, + json=update_data + ) + response_data = response.json() + + # assert + assert response.status_code == 200 + + # Prepare expected response + expected_response = { + "id": test_project.id, + "name": updated_name, # Updated name + "slug": test_project.slug, + "description": test_project.description, + "ai_providers": [provider.model_dump() for provider in test_project.ai_providers], + "tags": test_project.tags, + "org_id": test_project.org_id, + "owner": test_project.owner, + "total_transactions": 0, + "total_cost": 0, + "created_at": test_project.created_at.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + } + + # Compare datetime strings after formatting + assert response_data["created_at"].startswith(expected_response["created_at"]) + + # Remove created_at for the rest of the comparison + del response_data["created_at"] + del expected_response["created_at"] + + assert response_data == expected_response + +def test_update_project_with_multiple_fields_returns_200_and_updated_data(client, application, test_project): + """Test updating multiple project fields returns 200 and correctly updated project data""" + # arrange + update_data = { + "name": "Enterprise AI Solutions", + "description": "Advanced AI integration for enterprise clients", + "tags": ["enterprise", "production", "gpt4"], + "ai_providers": [ + { + "deployment_name": "gpt4-prod", + "slug": "gpt4-prod", + "api_base": "https://api.openai.com/v1", + "description": "Production GPT-4 deployment", + "provider_name": "OpenAI Enterprise" + }, + { + "deployment_name": "azure-gpt4", + "slug": "azure-gpt4", + "api_base": "https://azure.openai.com/v1", + "description": "Azure GPT-4 backup deployment", + "provider_name": "Azure OpenAI" + } + ] + } + + # act + response = client.put( + f"/api/projects/{test_project.id}", + headers=header, + json=update_data + ) + response_data = response.json() + + # assert + assert response.status_code == 200 + + # Prepare expected response + expected_response = { + "id": test_project.id, + "name": update_data["name"], + "slug": test_project.slug, # Slug remains unchanged + "description": update_data["description"], + "ai_providers": update_data["ai_providers"], + "tags": update_data["tags"], + "org_id": test_project.org_id, # Organization remains unchanged + "owner": test_project.owner, # Owner remains unchanged + "total_transactions": 0, + "total_cost": 0, + "created_at": test_project.created_at.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + } + + # Compare datetime strings after formatting + assert response_data["created_at"].startswith(expected_response["created_at"]) + + # Remove created_at for the rest of the comparison + del response_data["created_at"] + del expected_response["created_at"] + + assert response_data == expected_response + +def test_delete_project_with_valid_id_returns_204(client, application, test_project): + """Test deleting a project with valid ID returns 204""" + # act + response = client.delete(f"/api/projects/{test_project.id}", headers=header) + + # assert + assert response.status_code == 204 + +def test_delete_project_with_nonexistent_id_returns_204(client, application): + """Test deleting a non-existing project returns 204""" + # arrange + ... + + # act + response = client.delete(f"/api/projects/non-existing", headers=header) + + # assert + assert response.status_code == 204 + +def test_get_projects_with_existing_project_returns_200_and_project_list(client, application, test_project): + """Test retrieving all projects returns 200 and list containing the test project""" + # act + response = client.get("/api/projects", headers=header) + response_data = response.json() + + # assert + assert response.status_code == 200 + assert len(response_data) == 1 # Should contain only our test project + + # Prepare expected project data + expected_project = { + "id": test_project.id, + "name": test_project.name, + "slug": test_project.slug, + "description": test_project.description, + "ai_providers": [provider.model_dump() for provider in test_project.ai_providers], + "tags": test_project.tags, + "org_id": test_project.org_id, + "owner": test_project.owner, + "total_transactions": 0, + "total_cost": 0, + "created_at": test_project.created_at.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + } + + # Compare datetime strings after formatting + assert response_data[0]["created_at"].startswith(expected_project["created_at"]) + + # Remove created_at for the rest of the comparison + del response_data[0]["created_at"] + del expected_project["created_at"] + + assert response_data[0] == expected_project diff --git a/backend/tests/test_api_statistics_speed.py b/backend/tests/test_api_statistics_speed.py new file mode 100644 index 00000000..f6d9cb7d --- /dev/null +++ b/backend/tests/test_api_statistics_speed.py @@ -0,0 +1,1406 @@ +import pytest +from datetime import datetime +from test_utils import read_transactions_from_csv, truncate_float + +class TestBaseTransactionSpeed: + """Base test class for transaction speed statistics tests.""" + + @pytest.fixture(autouse=True) + def setup(self, client, application): + """Setup test data and common test fixtures.""" + self.client = client + self.application = application + self.header = { + "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyMTAyODc3OTUzNDg0MzUyNDI3IiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiYm54OW9WT1o4U3FJOTcyczBHYjd4dyIsIm5hbWUiOiJUZXN0IFVzZXIiLCJwaWN0dXJlIjoiIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJpYXQiOjE3MTM3MzQ0NjEsImV4cCI6OTk5OTk5OTk5OX0.eZYMQzcSRzkAq4Me8C6SNU3wduS7EIu_o5XGAbsDmU05GtyipQEb5iNJ1QiLg-11RbZFL3dvi8xKd3mpuw8b-5l6u8hwSpZg6wNPLY0zPX-EOwxeHLtev_2X5pUf1_IWAnso9K_knsK8CcmJoVsCyNNjlw3hrkChacJHGNzg0TTT1rh3oe6KCpbLvYlV6tUPfm5k3AMFZIT7Jntr38CZvs6gac6L_DhItJc3TNNUUHie2zgA29_r9YFlaEr_nGoSmBhIi-i0i0h34TL4JAb4qJkVM2YI2eTTv2HjEGtkx4mE5JvNQ0VxzHSJcCNOHh1gCiFD5c6rhvvxVeEqMkGGbCZKHX_vCgnIp0iE_OWyICjVTFPitQJ00fXLhyHyPb7q5J605tuK2iTHp2NCRJEXIAl9e0F_qASBBAfyL0C4FCBtvbnEMwtpoV1VWinkKgkI7JVH0AsyTugjXyAjxxsJxBTJT9qwZLxVBoaxgqNTOFfxvwstyq1VfCl3iBbpt71D" + } + + # Load and add test transactions + with self.application.transaction_context() as ctx: + repo = ctx["transaction_repository"] + repo.delete_cascade(project_id="project-test") + transactions = read_transactions_from_csv( + "test_transactions_tokens_cost_speed.csv" + ) + for transaction in transactions: + repo.add(transaction) + + yield + + # Cleanup test data + with self.application.transaction_context() as ctx: + repo = ctx["transaction_repository"] + repo.delete_cascade(project_id="project-test") + + def make_request(self, period, date_from, date_to, project_id="project-test"): + """Helper method to make API request for speed statistics.""" + + if date_from is None: + date_from = "" + if date_to is None: + date_to = "" + + api_url = f"/api/statistics/transactions_speed?" + #create api url add parameters if they are not empty + + if project_id != "": + api_url += f"project_id={project_id}" + + if period != "": + api_url += f"&period={period}" + if date_from != "": + api_url += f"&date_from={date_from}" + if date_to != "": + api_url += f"&date_to={date_to}" + + + return self.client.get( + api_url, + headers=self.header, + ) + + def extract_tokens_per_second(self, response): + """Helper method to extract tokens per second from response.""" + return list( + [ + tuple( + [ + truncate_float(record["tokens_per_second"], 3) + for record in date["records"] + ] + ) + for date in response.json() + ] + ) + + def assert_response(self, response, expected_count, expected_speeds): + """Helper method to verify response status, length and speed values.""" + assert response.status_code == 200 + assert len(response.json()) == expected_count + + actual_speeds = self.extract_tokens_per_second(response) + expected_speeds = [ + tuple(map(lambda x: truncate_float(x, 3), list(val))) + for val in expected_speeds + ] + + assert actual_speeds == expected_speeds + + +class TestTransactionSpeedAPIContract(TestBaseTransactionSpeed): + """Tests for API contract in transaction speed statistics. Check for errors and invalid parameters and contract for returned data.""" + + def test_speed_statistics_for_not_existing_project(self): + """ + Tests transaction speed statistics for a not existing project. + + Given: A set of transactions for a different project + When: Requesting speed statistics for non-existent project + Then: Returns 404 error with "Project not found" message + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:04:59", + "project-that-not-exists-xxx" + ) + + #current implementation returns 200 and empty list + assert response.status_code == 200 + assert len(response.json()) == 0 + + # TODO: this should be changed in the future + # assert response.status_code == 404 + # assert response.json() == {"error": "Project not found"} + + def test_speed_statistics_without_project_id(self): + """ + Tests transaction speed statistics without project_id. + + Given: A set of transactions + When: Requesting speed statistics without project_id + Then: Returns 422 error with "project_id is required" message + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:04:59", + "" + ) + + resp_data = response.json() + + assert response.status_code == 422 + assert resp_data['detail'][0]['msg'] == 'Field required' + + def test_speed_statistics_wrong_period(self): + """ + Tests transaction speed statistics for an invalid period parameter. + + Given: A set of valid transactions + When: Requesting speed statistics with invalid period + Then: Returns 422 error with validation details + """ + response = self.make_request( + "not-existing-period", + "2023-11-01T12:00:00", + "2023-11-01T12:29:59" + ) + + assert response.status_code == 422 + assert response.json()['detail'][0]['input'] == 'not-existing-period' + + def test_speed_statistics_for_date_from_after_date_to(self): + """ + Tests transaction speed statistics with invalid date range. + + Given: A set of valid transactions + When: Requesting speed statistics with date_from after date_to + Then: Returns 400 error with invalid date range message + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:10:00", + "2023-11-01T12:00:00" + ) + + assert response.status_code == 400 + assert response.json()['detail'] == "date_from cannot be after date_to" + + def test_speed_statistics_date_from_required(self): + """ + Tests transaction speed statistics without date_from. + + Given: A set of valid transactions + When: Requesting speed statistics without date_from + Then: Returns 422 error with "date_from is required" message + """ + response = self.make_request( + "5minutes", + None, + "2023-11-01T12:00:00" + ) + + resp_data = response.json() + + assert response.status_code == 422 + assert resp_data['detail'][0]['msg'] == "Field required" + + def test_speed_statistics_not_valid_dates_from(self): + """ + Tests transaction speed statistics with not valid date_from. + + Given: A set of valid transactions + When: Requesting speed statistics with not valid date_from + Then: Returns 422 error with validation details + """ + response = self.make_request( + "5minutes", + "2023-11-31T12:00:00", + "2023-11-31T12:10:00" + ) + + resp_data = response.json() + + assert response.status_code == 400 + assert resp_data['detail'] == "Invalid date format. Use YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS: day is out of range for month" + + def test_speed_statistics_date_to_required(self): + """ + Tests transaction speed statistics without date_to. + + Given: A set of valid transactions + When: Requesting speed statistics without date_to + Then: Returns 422 error with "date_to is required" message + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + None + ) + + resp_data = response.json() + + assert response.status_code == 422 + assert resp_data['detail'][0]['msg'] == "Field required" + + def test_speed_statistics_dates_not_iso_format(self): + """ + Tests transaction speed statistics with dates not in ISO format. + + Given: A set of valid transactions + When: Requesting speed statistics with dates not in ISO format + Then: Returns 400 error with invalid date format message + """ + response_datetime = self.make_request( + "5minutes", + datetime(2023, 11, 1, 12, 0, 0), # not ISO format 2023-11-02 12:00:00 (without "T" between date and time) + datetime(2023, 11, 2, 12, 0, 0) # not ISO format 2023-11-02 12:00:00 (without "T" between date and time) + ) + + assert response_datetime.status_code == 400 + assert response_datetime.json()['detail'] == "Invalid date format. Use YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS: Invalid date format" + + + def test_speed_statistics_date_without_time_is_same_as_with_time(self): + """ + Tests transaction speed statistics with date_from without time is same as with time. + + Given: A set of valid transactions + When: Requesting speed statistics with date_from without time and with time + Then: Time shoud be added automatically as 00:00:00 and returns same data + """ + response_without_time = self.make_request( + "5minutes", + "2023-11-01", + "2023-11-02" + ) + + response_with_time = self.make_request( + "5minutes", + "2023-11-01T00:00:00", + "2023-11-02T00:00:00" + ) + + assert response_without_time.status_code == 200 + assert response_with_time.status_code == 200 + + + data_without_time = response_without_time.json() + data_with_time = response_with_time.json() + + assert len(data_without_time) == 289 + assert len(data_without_time) == len(data_with_time) + + assert data_without_time == data_with_time + + def test_speed_statistics_5min_granularity_2_data_keypoints(self): + """ + Tests transaction speed statistics returned data contract for 5-minute granularity in 9:59 minutes period. + + Given: Transactions for 9:59 minutes period + When: Requesting speed statistics with 5-minute granularity + Then: Returns 2 intervals with proper data keys and records with all the fields according to the contract + """ + + + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:09:59" + ) + resp_data = response.json() + + expected_dates = [ datetime.fromisoformat("2023-11-01T12:00:00"), + datetime.fromisoformat("2023-11-01T12:05:00"), + ] + + response_dates = [ datetime.fromisoformat(resp_data[0]['date']), + datetime.fromisoformat(resp_data[1]['date']) + ] + + assert len(resp_data) == 2 + assert response_dates == expected_dates + assert len(resp_data[0]['records']) == 3 + assert len(resp_data[1]['records']) == 3 + + records_0 = resp_data[0]['records'] + records_1 = resp_data[1]['records'] + + + #provider Azure OpenAI gpt-35-turbo-0613 + assert records_0[0]['provider'] == 'Azure OpenAI' + assert records_0[0]['model'] == 'gpt-35-turbo-0613' + assert records_0[0]['mean_latency'] == pytest.approx(6.0, rel=1e-3) + assert records_0[0]['tokens_per_second'] == pytest.approx(31.77272727, rel=1e-3) + assert records_0[0]['total_transactions'] == 2 + + #provider OpenAI babbage-002 + assert records_0[1]['provider'] == 'OpenAI' + assert records_0[1]['model'] == 'babbage-002' + assert records_0[1]['mean_latency'] == pytest.approx(0.0, rel=1e-3) + assert records_0[1]['tokens_per_second'] == pytest.approx(0.0, rel=1e-3) + assert records_0[1]['total_transactions'] == 0 + + #provider OpenAI gpt-4-0613 + assert records_0[2]['provider'] == 'OpenAI' + assert records_0[2]['model'] == 'gpt-4-0613' + assert records_0[2]['mean_latency'] == pytest.approx(0.0, rel=1e-3) + assert records_0[2]['tokens_per_second'] == pytest.approx(0.0, rel=1e-3) + assert records_0[2]['total_transactions'] == 0 + + #provider Azure OpenAI gpt-35-turbo-0613 + assert records_1[0]['provider'] == 'Azure OpenAI' + assert records_1[0]['model'] == 'gpt-35-turbo-0613' + assert records_1[0]['mean_latency'] == pytest.approx(120.999, rel=1e-3) + assert records_1[0]['tokens_per_second'] == pytest.approx(2.9421730, rel=1e-3) + assert records_1[0]['total_transactions'] == 1 + + #provider OpenAI babbage-002 + assert records_1[1]['provider'] == 'OpenAI' + assert records_1[1]['model'] == 'babbage-002' + assert records_1[1]['mean_latency'] == pytest.approx(10.0, rel=1e-3) + assert records_1[1]['tokens_per_second'] == pytest.approx(7.7, rel=1e-3) + assert records_1[1]['total_transactions'] == 1 + + #provider OpenAI gpt-4-0613 + assert records_1[2]['provider'] == 'OpenAI' + assert records_1[2]['model'] == 'gpt-4-0613' + assert records_1[2]['mean_latency'] == pytest.approx(2.0, rel=1e-3) + assert records_1[2]['tokens_per_second'] == pytest.approx(16.0, rel=1e-3) + assert records_1[2]['total_transactions'] == 1 + + def test_speed_statistics_5min_granularity_3_data_keypoints(self): + """ + Tests transaction speed statistics returned data contract for 5-minute granularity in 10 minutes period. + + Given: Transactions for 10 minutes period + When: Requesting speed statistics with 5-minute granularity + Then: Returns proper number of intervals with proper data keys and records with all the fields according to the contract + """ + + + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:10:00" + ) + resp_data = response.json() + + expected_dates = [ datetime.fromisoformat("2023-11-01T12:00:00"), + datetime.fromisoformat("2023-11-01T12:05:00"), + datetime.fromisoformat("2023-11-01T12:10:00"), + ] + + response_dates = [ datetime.fromisoformat(resp_data[0]['date']), + datetime.fromisoformat(resp_data[1]['date']), + datetime.fromisoformat(resp_data[2]['date']) + ] + + assert len(resp_data) == 3 + assert response_dates == expected_dates + + assert len(resp_data[0]['records']) == 3 + assert len(resp_data[1]['records']) == 3 + assert len(resp_data[2]['records']) == 3 + + records_0 = resp_data[0]['records'] + records_1 = resp_data[1]['records'] + records_2 = resp_data[2]['records'] + + #goes for all records in 3 data points, + for i in range(3): + # check if records contain the keys as expected + assert all(key in records_0[i] for key in ['provider', 'model', 'mean_latency', 'tokens_per_second', 'total_transactions']) + assert all(key in records_1[i] for key in ['provider', 'model', 'mean_latency', 'tokens_per_second', 'total_transactions']) + assert all(key in records_2[i] for key in ['provider', 'model', 'mean_latency', 'tokens_per_second', 'total_transactions']) + + # checks if all the providers and models are the same in all 3 data points + assert records_0[i]['provider'] == records_1[i]['provider'] == records_2[i]['provider'] + assert records_0[i]['model'] == records_1[i]['model'] == records_2[i]['model'] + + + @pytest.mark.parametrize("period,date_from,date_to,expected_dates", [ + # 5 minutes granularity + ( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:10:00", + [ + "2023-11-01T12:00:00", + "2023-11-01T12:05:00", + "2023-11-01T12:10:00" + ] + ), + ( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:09:59", + [ + "2023-11-01T12:00:00", + "2023-11-01T12:05:00", + ] + ), + ( # period starts in the middle of the interval + "5minutes", + "2023-11-01T12:02:00", + "2023-11-01T12:09:59", + [ + "2023-11-01T12:00:00", + "2023-11-01T12:05:00", + ] + ), + ( + "5minutes", + "2023-11-01T23:55:00", + "2023-11-02T00:00:00", + [ + # empty data in this period + ] + ), + # hourly granularity + ( + "hour", + "2023-11-01T12:00:00", + "2023-11-01T13:00:00", + [ + "2023-11-01T12:00:00", + "2023-11-01T13:00:00" + ] + ), + ( + "hour", + "2023-11-01T12:00:00", + "2023-11-01T14:00:00", + [ + "2023-11-01T12:00:00", + "2023-11-01T13:00:00", + "2023-11-01T14:00:00" + ] + ), + ( + "hour", + "2023-11-01T12:00:00", + "2023-11-01T13:59:59", + [ + "2023-11-01T12:00:00", + "2023-11-01T13:00:00", + ] + ), + ( # period starts in the middle of the interval + "hour", + "2023-11-01T12:30:00", + "2023-11-01T13:30:00", + [ + "2023-11-01T12:00:00", + "2023-11-01T13:00:00", + ] + ), + ( # period starts in the middle of the interval + "hour", + "2023-11-01T11:59:59", + "2023-11-01T13:30:00", + [ + "2023-11-01T11:00:00", + "2023-11-01T12:00:00", + "2023-11-01T13:00:00", + ] + ), + + ( + "hour", + "2023-11-01T23:00:00", + "2023-11-02T00:00:00", + [ + #empty data in this period + ] + ), + # daily granularity + ( + "day", + "2023-11-01T00:00:00", + "2023-11-03T00:00:00", + [ + "2023-11-01T00:00:00", + "2023-11-02T00:00:00", + "2023-11-03T00:00:00" + ] + ), + ( + "day", + "2023-11-01T12:00:00", + "2023-11-02T12:00:00", + [ + "2023-11-01T00:00:00", + "2023-11-02T00:00:00", + ] + ), + ( + "day", + "2023-11-01", + "2023-11-01", + [ + #"2023-11-01T00:00:00", + ] + ), + ( # period starts in the middle of the interval + "day", + "2023-11-30T23:59:59", + "2023-12-02T12:00:00", + [ + "2023-11-30T00:00:00", + "2023-12-01T00:00:00", + "2023-12-02T00:00:00", + ] + ), + # weekly granularity + ( # each week starts on a monday, we should return as a data point the end of the week (Monday 00:00:00) + "week", + "2023-11-01T00:00:00", + "2023-11-15T00:00:00", + [ + "2023-11-06T00:00:00", + "2023-11-13T00:00:00", + "2023-11-20T00:00:00" + ] + ), + ( # each week starts on a monday, we should return as a data point the end of the week (Monday 00:00:00) + "week", + "2023-11-06T00:00:00", + "2023-11-15T00:00:00", + [ + "2023-11-06T00:00:00", + "2023-11-13T00:00:00", + "2023-11-20T00:00:00" + ] + ), + ( # each week starts on a monday, we should return as a data point the end of the week (Monday 00:00:00) + "week", + "2023-11-06T00:00:01", + "2023-11-15T00:00:00", + [ + "2023-11-06T00:00:00", + "2023-11-13T00:00:00", + "2023-11-20T00:00:00" + ] + ), + ( # each week starts on a + "week", + "2023-11-06T15:00:00", + "2023-11-13T23:59:59", + [ + "2023-11-06T00:00:00", + "2023-11-13T00:00:00", + ] + ), + ( + "week", + "2023-11-07T00:00:00", + "2023-11-15T00:00:00", + [ + "2023-11-13T00:00:00", + "2023-11-20T00:00:00" + ] + ), + # monthly granularity + ( + "month", + "2023-11-01T00:00:00", + "2024-01-01T00:00:00", + [ + "2023-11-30T00:00:00", + "2023-12-31T00:00:00", + "2024-01-31T00:00:00" + ] + ), + ( + "month", + "2023-11-01T00:00:00", + "2024-01-02T00:00:00", + [ + "2023-11-30T00:00:00", + "2023-12-31T00:00:00", + "2024-01-31T00:00:00" + ] + ), + ( + "month", + "2023-10-31T00:00:00", + "2023-12-02T00:00:00", + [ + "2023-10-31T00:00:00", + "2023-11-30T00:00:00", + "2023-12-31T00:00:00" + ] + ), + ( + "month", + "2023-11-30T00:00:00", + "2023-11-30T23:59:59", + [ + "2023-11-30T00:00:00", + ] + ), + ( + "month", + "2023-02-15T00:00:00", + "2023-04-30T23:59:59", + [ + # empty data in this period + ] + ), + # yearly granularity + ( + "year", + "2023-01-01T00:00:00", + "2025-01-01T00:00:00", + [ + "2023-12-31T00:00:00", + "2024-12-31T00:00:00", + "2025-12-31T00:00:00" + ] + ), + ( + "year", + "2023-12-31T23:59:59", + "2024-01-01T00:00:00", + [ + "2023-12-31T00:00:00", + "2024-12-31T00:00:00", + ] + ), + ]) + def test_speed_statistics_data_points_arrangement(self, period, date_from, date_to, expected_dates): + """ + Tests transaction speed statistics returned data contract for different time granularities. + + Given: Transactions for different time periods + When: Requesting speed statistics with different granularities (5min, hourly, daily, weekly, monthly, yearly) + Then: Returns correct number of intervals with proper data keys and records with all the fields according to the contract + """ + response = self.make_request( + period, + date_from, + date_to + ) + resp_data = response.json() + + expected_dates = [datetime.fromisoformat(date) for date in expected_dates] + response_dates = [datetime.fromisoformat(date_keypoint['date']) for date_keypoint in resp_data] + + assert len(resp_data) == len(expected_dates) + assert response_dates == expected_dates + + number_of_data_points = len(resp_data) + + # returns the number of models in the first data point or 0 if the data is empty + number_of_models = len(resp_data[0]['records']) if resp_data else 0 + + #goes for all records in all data points, + for i in range(number_of_models): + for j in range(number_of_data_points): + # check if records contain the keys as expected + assert all(key in resp_data[j]['records'][i] for key in ['provider', 'model', 'mean_latency', 'tokens_per_second', 'total_transactions']) + + + # cross checks if all the providers and models are the same in all data points, use transition property if data_point_0 == data_point_1 and data_point_1 == data_point_2 then data_point_0 == data_point_2 + for j in range(1, number_of_data_points): + for i in range(number_of_models): + assert resp_data[j-1]['records'][i]['provider'] == resp_data[j]['records'][i]['provider'] + assert resp_data[j-1]['records'][i]['model'] == resp_data[j]['records'][i]['model'] + + +class TestMinutesGranularity(TestBaseTransactionSpeed): + """Tests for 5-minute granularity transaction speed statistics.""" + + def test_0min_duration_with_5min_granularity_same_date(self): + """ + Tests transaction speed statistics for a 0-minute time frame. + + Given: A time period with zero duration (2023-11-01 12:00-12:00) + When: Requesting speed statistics with 5-minute granularity + Then: Returns empty list + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:00:00" + ) + + self.assert_response(response, 0, []) + + def test_5min_duration_with_5min_granularity_returns_single_interval(self): + """ + Tests transaction speed statistics for a 5-minute time frame. + + Given: Transactions spanning 5 minutes (2023-11-01 12:00-12:05) + When: Requesting speed statistics with 5-minute granularity + Then: Returns one interval with tokens per second for each model + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:04:59" + ) + + resp_data = response.json() + + + + validation = [tuple([31.77272037])] + self.assert_response(response, 1, validation) + + def test_5min_duration_with_5min_granularity_plus_one_hour_time_zone_returns_same_result(self): + """ + Tests transaction speed statistics for a 5-minute time frame check if we get the same result if the dates are in different time zones. UTC is the default time zone. + + Given: Transactions spanning 5 minutes (2023-11-01 12:00-12:05) + When: Requesting speed statistics with 5-minute granularity in UTC and CET + Then: Returns the same interval with tokens per second for each model in both time zones + """ + response_utc_0 = self.make_request( + "5minutes", + "2023-11-01T12:00:00+00:00", + "2023-11-01T12:04:59+00:00" + ) + + response_utc_1 = self.make_request( + "5minutes", + "2023-11-01T13:00:00+01:00", + "2023-11-01T13:04:59+01:00" + ) + + resp_data_utc_0 = response_utc_0.json() + resp_data_utc_1 = response_utc_1.json() + + assert resp_data_utc_0 == resp_data_utc_1 + + validation = [tuple([31.77272037])] + self.assert_response(response_utc_0, 1, validation) + + def test_5min_duration_with_5min_granularity_plus_one_minushour_time_zone_returns_same_result(self): + """ + Tests transaction speed statistics for a 5-minute time frame check if we get the same result if the dates are in different time zones. UTC is the default time zone. + + Given: Transactions spanning 5 minutes (2023-11-01 12:00-12:05) + When: Requesting speed statistics with 5-minute granularity in UTC and CET + Then: Returns the same interval with tokens per second for each model in both time zones + """ + response_utc_0 = self.make_request( + "5minutes", + "2023-11-01T11:00:00-01:00", + "2023-11-01T11:04:59-01:00" + ) + + response_utc_1 = self.make_request( + "5minutes", + "2023-11-01T13:00:00+01:00", + "2023-11-01T13:04:59+01:00" + ) + + resp_data_utc_0 = response_utc_0.json() + resp_data_utc_1 = response_utc_1.json() + + assert resp_data_utc_0 == resp_data_utc_1 + + validation = [tuple([31.77272037])] + self.assert_response(response_utc_0, 1, validation) + + + def test_30min_duration_with_5min_granularity_returns_six_intervals(self): + """ + Tests transaction speed statistics for a 30-minute time frame. + + Given: Transactions spanning 30 minutes (2023-11-01 12:00-12:30) + When: Requesting speed statistics with 5-minute granularity + Then: Returns six intervals with tokens per second for each model + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:29:59" + ) + + validation = [ + tuple([0, 31.77272037, 0, 0]), + tuple([0, 2.94217308, 7.700000136, 16.00000129]), + tuple([0, 0, 0, 0]), + tuple([65.13026753, 0, 0, 0]), + tuple([0, 0, 82.41802468, 0]), + tuple([92.09210559, 89.41178679, 0, 20.00000035]), + ] + self.assert_response(response, 6, validation) + + def test_1hour_duration_with_5min_granularity_returns_twelve_intervals(self): + """ + Tests transaction speed statistics for a 1-hour time frame. + + Given: Transactions spanning one hour (2023-11-01 12:00-13:00) + When: Requesting speed statistics with 5-minute granularity + Then: Returns twelve intervals with tokens per second for each model + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:59:59" + ) + + validation = [ + tuple([0, 31.77272037, 0, 0]), + tuple([0, 2.94217308, 7.700000136, 16.00000129]), + tuple([0, 0, 0, 0]), + tuple([65.13026753, 0, 0, 0]), + tuple([0, 0, 82.41802468, 0]), + tuple([92.09210559, 89.41178679, 0, 20.00000035]), + tuple([0, 0, 0, 0]), + tuple([0, 98.46880746, 0, 0]), + tuple([0, 67.43749926, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 38.45918092, 0, 0]), + tuple([0, 0, 80.65413879, 55.3845944]), + ] + self.assert_response(response, 12, validation) + + def test_1month_duration_with_5min_granularity_returns_empty_list(self): + """ + Tests transaction speed statistics for a period with no data. + + Given: A time period with no recorded transactions (October 2023) + When: Requesting speed statistics with 5-minute granularity + Then: Returns an empty list + """ + response = self.make_request( + "5minutes", + "2023-10-01T12:00:00", + "2023-10-31T12:30:00" + ) + + self.assert_response(response, 0, []) + + +class TestHourlyGranularity(TestBaseTransactionSpeed): + """Tests for hourly granularity transaction speed statistics.""" + + def test_30min_duration_with_hourly_granularity_returns_single_interval(self): + """ + Tests transaction speed statistics for a 30-minute time frame. + + Given: Transactions spanning 30 minutes (2023-11-01 12:00-12:30) + When: Requesting speed statistics with hourly granularity + Then: Returns one interval with tokens per second for each model + """ + response = self.make_request( + "hour", + "2023-11-01T12:00:00", + "2023-11-01T12:30:00" + ) + + validation = [tuple([78.61118656, 38.97485015, 57.5120165, 18.00000082])] + self.assert_response(response, 1, validation) + + def test_1hour_duration_with_hourly_granularity_returns_two_intervals(self): + """ + Tests transaction speed statistics for a 1-hour time frame. + + Given: Transactions spanning one hour (2023-11-01 12:00-13:00) + When: Requesting speed statistics with hourly granularity + Then: Returns two intervals with tokens per second for each model + """ + response = self.make_request( + "hour", + "2023-11-01T12:00:00", + "2023-11-01T13:00:00" + ) + + validation = [ + tuple([78.61118656, 48.46159236, 69.08307764, 30.46153202]), + tuple([0, 0, 0, 0]), + ] + self.assert_response(response, 2, validation) + + def test_24hour_duration_with_hourly_granularity_returns_24_intervals(self): + """ + Tests transaction speed statistics for a 24-hour time frame. + + Given: Transactions spanning 24 hours (2023-11-01 12:00 to 2023-11-02 12:00) + When: Requesting speed statistics with hourly granularity + Then: Returns 24 intervals with tokens per second for each model + """ + response = self.make_request( + "hour", + "2023-11-01T12:00:00", + "2023-11-02T11:59:59" + ) + + validation = [ + tuple([78.61118656, 48.46159236, 69.08307764, 30.46153202]), + tuple([0, 4.124278, 0, 0]), + tuple([0, 41.66667439, 0, 0]), + tuple([97.51860847, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([52.25000421, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([53.2270015, 23.00000908, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 5.988021267, 44.1666656]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([31.50684656, 0, 47.52925421, 0]), + tuple([0, 0, 0, 0]), + tuple([57.48084108, 0, 0, 0]), + tuple([0, 0, 32.00001264, 0]), + ] + self.assert_response(response, 24, validation) + + def test_1month_duration_with_hourly_granularity_returns_empty_list(self): + """ + Tests transaction speed statistics for a period with no data. + + Given: A time period with no recorded transactions (October 2023) + When: Requesting speed statistics with hourly granularity + Then: Returns an empty list + """ + response = self.make_request( + "hour", + "2023-10-01T12:00:00", + "2023-10-31T18:00:00" + ) + + self.assert_response(response, 0, []) + + def test_1day_duration_with_daily_granularity_returns_two_intervals(self): + """ + Tests transaction speed statistics for a 1-day time frame. + + Given: Transactions spanning one day (2023-11-01 13:00 to 2023-11-02 13:00) + When: Requesting speed statistics with daily granularity + Then: Returns two intervals with tokens per second for each model + """ + response = self.make_request( + "day", + "2023-11-01T13:00:00", + "2023-11-02T13:00:00" + ) + + validation = [ + tuple([67.66520472, 22.93032058, 0, 0]), + tuple([54.33745498, 0, 36.11515931, 44.1666656]), + ] + self.assert_response(response, 2, validation) + + def test_1month_duration_with_daily_granularity_returns_30_intervals(self): + """ + Tests transaction speed statistics for a 1-month time frame. + + Given: Transactions spanning one month (2023-11-01 12:00 to 2023-11-30 12:00) + When: Requesting speed statistics with daily granularity + Then: Returns 30 intervals with tokens per second for each model + """ + response = self.make_request( + "day", + "2023-11-01T12:00:00", + "2023-11-30T12:00:00" + ) + + validation = [ + tuple([72.04359746, 43.355338, 69.08307764, 30.46153202]), + tuple([49.37809126, 0, 36.11515931, 44.1666656]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([23.46938859, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 73.48172621]), + tuple([0, 0, 86.68341626, 5.546371285]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 30.14705791, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 30.77519425, 0, 0]), + tuple([0, 79.79794645, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 14.50000117]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 83.11703415]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 73.73913033, 0]), + ] + self.assert_response(response, 30, validation) + + def test_1day_duration_with_daily_granularity_returns_empty_list(self): + """ + Tests transaction speed statistics for a period with no data. + + Given: A time period with no recorded transactions (2023-11-03 12:00 to 2023-11-04 12:00) + When: Requesting speed statistics with daily granularity + Then: Returns an empty list + """ + response = self.make_request( + "day", + "2023-11-03T12:00:00", + "2023-11-04T12:00:00" + ) + + self.assert_response(response, 0, []) + +class TestDailyGranularity(TestBaseTransactionSpeed): + """Tests for daily granularity transaction speed statistics.""" + + def test_6hour_duration_with_daily_granularity_returns_single_interval(self): + """ + Tests transaction speed statistics for a 6-hour time frame. + + Given: Transactions spanning 6 hours (2023-11-01 12:00-18:00) + When: Requesting speed statistics with daily granularity + Then: Returns one interval with tokens per second for each model + """ + response = self.make_request( + "day", + "2023-11-01T12:00:00", + "2023-11-01T18:00:00" + ) + + validation = [tuple([76.74774645, 44.80929007, 69.08307764, 30.46153202])] + self.assert_response(response, 1, validation) + + def test_24hour_duration_with_daily_granularity_returns_single_interval(self): + """ + Tests transaction speed statistics for a 24-hour time frame. + + Given: Transactions spanning 24 hours (2023-11-01 00:00-23:59) + When: Requesting speed statistics with daily granularity + Then: Returns one interval with tokens per second for each model + """ + response = self.make_request( + "day", + "2023-11-01T00:00:00", + "2023-11-01T23:59:59" + ) + + resp_data = response.json() + + response_date = datetime.fromisoformat(resp_data[0]['date']) + expected_date = datetime.fromisoformat("2023-11-01T00:00:00") + assert response_date == expected_date + + validation = [tuple([72.04359746, 43.355338, 69.08307764, 30.46153202])] + self.assert_response(response, 1, validation) + + def test_7day_duration_with_daily_granularity_returns_seven_intervals(self): + """ + Tests transaction speed statistics for a 7-day time frame. + + Given: Transactions spanning 7 days (2023-11-01 to 2023-11-07) + When: Requesting speed statistics with daily granularity + Then: Returns seven intervals with tokens per second for each model + """ + response = self.make_request( + "day", + "2023-11-01T12:00:00", + "2023-11-07T11:59:59" + ) + + validation = [ + tuple([72.04359746, 43.355338, 69.08307764, 30.46153202]), + tuple([49.37809126, 0, 36.11515931, 44.1666656]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([23.46938859, 0, 0, 0]), + tuple([0, 0, 0, 0]), + ] + self.assert_response(response, 7, validation) + + def test_1month_duration_with_daily_granularity_returns_empty_list(self): + """ + Tests transaction speed statistics for a period with no data. + + Given: A time period with no recorded transactions (2024-05-01 to 2024-06-01) + When: Requesting speed statistics with daily granularity + Then: Returns an empty list + """ + response = self.make_request( + "day", + "2024-05-01", + "2024-06-01" + ) + + self.assert_response(response, 0, []) + + +class TestWeeklyGranularity(TestBaseTransactionSpeed): + """Tests for weekly granularity transaction speed statistics.""" + + def test_3day_duration_with_weekly_granularity_returns_single_interval(self): + """ + Tests transaction speed statistics for a 3-day time frame. + + Given: Transactions spanning 3 days (2023-11-01 to 2023-11-03) + When: Requesting speed statistics with weekly granularity + Then: Returns one interval with tokens per second for each model + """ + response = self.make_request( + "week", + "2023-11-01T12:00:00", + "2023-11-03T12:00:00" + ) + + validation = [tuple([61.97003915, 43.355338, 54.09766022, 33.88781541])] + self.assert_response(response, 1, validation) + + def test_7day_duration_with_weekly_granularity_returns_two_intervals(self): + """ + Tests transaction speed statistics for a 7-day time frame. + + Given: Transactions spanning 7 days (2023-11-01 to 2023-11-07) + When: Requesting speed statistics with weekly granularity + Then: Returns two intervals with tokens per second for each model + """ + response = self.make_request( + "week", + "2023-11-01T12:00:00", + "2023-11-07T12:00:00" + ) + + validation = [ + tuple([58.11997409, 43.355338, 54.09766022, 33.88781541]), + tuple([0, 0, 0, 0]), + ] + self.assert_response(response, 2, validation) + + def test_2month_duration_with_weekly_granularity_returns_nine_intervals(self): + """ + Tests transaction speed statistics for a 2-month time frame. + + Given: Transactions spanning 2 months (2023-11-01 to 2023-12-31) + When: Requesting speed statistics with weekly granularity + Then: Returns nine intervals with tokens per second for each model + """ + response = self.make_request( + "week", + "2023-11-01T12:00:00", + "2023-12-31T00:00:00" + ) + + validation = [ + tuple([58.11997409, 43.355338, 54.09766022, 33.88781541]), + tuple([0, 0, 86.68341626, 50.8366079]), + tuple([0, 55.28657035, 30.14705791, 0]), + tuple([0, 0, 0, 14.50000117]), + tuple([0, 0, 49.86956757, 57.39410621]), + tuple([54.36081291, 45.90951084, 0, 0]), + tuple([11.46285769, 72.06896586, 0, 0]), + tuple([0, 0, 0, 0]), + tuple([0, 0, 0, 0]), + ] + self.assert_response(response, 9, validation) + + def test_2week_duration_with_weekly_granularity_returns_empty_list(self): + """ + Tests transaction speed statistics for a period with no data. + + Given: A time period with no recorded transactions (2024-03-29 to 2024-04-12) + When: Requesting speed statistics with weekly granularity + Then: Returns an empty list + """ + response = self.make_request( + "week", + "2024-03-29T00:00:00", + "2024-04-12T00:00:00" + ) + + self.assert_response(response, 0, []) + + +class TestMonthlyGranularity(TestBaseTransactionSpeed): + """Tests for monthly granularity transaction speed statistics.""" + + def test_7day_duration_with_monthly_granularity_returns_single_interval(self): + """ + Tests transaction speed statistics for a 7-day time frame. + + Given: Transactions spanning 7 days (2023-11-01 to 2023-11-07) + When: Requesting speed statistics with monthly granularity + Then: Returns one interval with tokens per second for each model + """ + response = self.make_request( + "month", + "2023-11-01T12:00:00", + "2023-11-07T12:00:00" + ) + + validation = [tuple([58.119971, 43.355338, 54.09766022, 33.88781541])] + self.assert_response(response, 1, validation) + + + def test_1month_duration_with_monthly_granularity_returns_single_interval(self): + """ + Tests transaction speed statistics for a 1-month time frame. + + Given: Transactions spanning one month (2023-11-01 to 2023-11-30) + When: Requesting speed statistics with monthly granularity + Then: Returns one interval with tokens per second for each model + """ + response = self.make_request( + "month", + "2023-11-01", + "2023-11-30T23:59:59" + ) + + validation = [tuple([58.11997409, 44.75901239, 56.11741906, 42.85312452])] + self.assert_response(response, 1, validation) + + def test_1month_duration_with_monthly_granularity_returns_empty_list(self): + """ + Tests transaction speed statistics for a period with no data. + + Given: A time period with no recorded transactions (2024-05-01 to 2024-06-01) + When: Requesting speed statistics with monthly granularity + Then: Returns an empty list + """ + response = self.make_request( + "month", + "2024-05-01", + "2024-06-01" + ) + + self.assert_response(response, 0, []) + + + + def test_6month_duration_with_monthly_granularity_returns_six_intervals(self): + """ + Tests transaction speed statistics for a 6-month time frame. + + Given: Transactions spanning 6 months (2023-10-01 to 2024-03-31) + When: Requesting speed statistics with monthly granularity + Then: Returns six intervals with tokens per second for each model + """ + response = self.make_request( + "month", + "2023-10-01", + "2024-03-31" + ) + validation = [ + tuple([0, 0, 0, 0]), + tuple([58.11997409, 44.75901239, 56.11741906, 42.85312452]), + tuple([32.9118353, 55.815765, 26.00000482, 44.53264224]), + tuple([38.85257592, 11.75, 17.26086978, 69.94543473]), + tuple([0, 0, 0, 0]), + tuple([55.78817778, 0, 0, 60.88811508]), + ] + self.assert_response(response, 6, validation) + + +class TestYearlyGranularity(TestBaseTransactionSpeed): + """Tests for yearly granularity transaction speed statistics.""" + + def test_2month_duration_with_yearly_granularity_returns_single_interval(self): + """ + Tests transaction speed statistics for a 2-month time frame. + + Given: Transactions spanning 2 months (2023-11-01 to 2023-12-31) + When: Requesting speed statistics with yearly granularity + Then: Returns one interval with tokens per second for each model + """ + response = self.make_request( + "year", + "2023-11-01", + "2023-12-31T23:59:59" + ) + + resp_data = response.json() + + expected_date = datetime.fromisoformat("2023-12-31T00:00:00") + response_date = datetime.fromisoformat(resp_data[0]['date']) + + assert len(resp_data) == 1 + assert response_date == expected_date + assert len(resp_data[0]['records']) == 4 + + records = resp_data[0]['records'] + + #provider Anthropic claude-2.0 + assert records[0]['provider'] == 'Anthropic' + assert records[0]['model'] == 'claude-2.0' + assert records[0]['mean_latency'] == pytest.approx(10.32475, rel=1e-3) + assert records[0]['tokens_per_second'] == pytest.approx(53.91861763, rel=1e-3) + assert records[0]['total_transactions'] == 12 + + #provider Azure OpenAI gpt-35-turbo-0613 + assert records[1]['provider'] == 'Azure OpenAI' + assert records[1]['model'] == 'gpt-35-turbo-0613' + assert records[1]['mean_latency'] == pytest.approx(14.68428, rel=1e-3) + assert records[1]['tokens_per_second'] == pytest.approx(44.82339021, rel=1e-3) + assert records[1]['total_transactions'] == 21 + + #provider OpenAI babbage-002 + assert records[2]['provider'] == 'OpenAI' + assert records[2]['model'] == 'babbage-002' + assert records[2]['mean_latency'] == pytest.approx(10.8833333, rel=1e-3) + assert records[2]['tokens_per_second'] == pytest.approx(54.10959145, rel=1e-3) + assert records[2]['total_transactions'] == 15 + + #provider OpenAI gpt-4-0613 + assert records[3]['provider'] == 'OpenAI' + assert records[3]['model'] == 'gpt-4-0613' + assert records[3]['mean_latency'] == pytest.approx(47.21509, rel=1e-3) + assert records[3]['tokens_per_second'] == pytest.approx(43.15849138, rel=1e-3) + assert records[3]['total_transactions'] == 11 + + + def test_5month_duration_with_yearly_granularity_returns_two_intervals(self): + """ + Tests transaction speed statistics for a 5-month time frame. + + Given: Transactions spanning from 2023 to 2024 (2023-11-01 to 2024-03-31) + When: Requesting speed statistics with yearly granularity + Then: Returns two intervals with tokens per second for each model + """ + response = self.make_request( + "year", + "2023-11-01", + "2024-03-31" + ) + + validation = [ + tuple([53.91861763, 47.27191158, 54.10959145, 43.15849138]), + tuple([48.5300627, 11.75, 17.26086978, 63.15244499]), + ] + self.assert_response(response, 2, validation) + + def test_6month_duration_with_yearly_granularity_returns_empty_list(self): + """ + Tests transaction speed statistics for a period with no data. + + Given: A time period with no recorded transactions (2024-05-31 to 2024-12-31) + When: Requesting speed statistics with yearly granularity + Then: Returns an empty list + """ + response = self.make_request( + "year", + "2024-05-31", + "2024-12-31" + ) + + self.assert_response(response, 0, []) + + def test_5month_duration_with_yearly_granularity_returns_two_intervals(self): + """ + Tests transaction speed statistics for a 5-month time frame with yearly granularity. + + Given: Transactions spanning from 2023 to 2024 (2023-11-01 to 2024-03-31) + When: Requesting transaction speed statistics with yearly granularity + Then: Returns two intervals with tokens per second for each model + """ + response = self.make_request( + "year", + "2023-11-01", + "2024-03-31" + ) + + validation = [ + tuple([53.91861763, 47.27191158, 54.10959145, 43.15849138]), + tuple([48.5300627, 11.75, 17.26086978, 63.15244499]), + ] + self.assert_response(response, 2, validation) + + + + \ No newline at end of file diff --git a/backend/tests/test_api_statistics_transaction_cost.py b/backend/tests/test_api_statistics_transaction_cost.py new file mode 100644 index 00000000..826b2da4 --- /dev/null +++ b/backend/tests/test_api_statistics_transaction_cost.py @@ -0,0 +1,965 @@ +import pytest +from test_utils import read_transactions_with_prices_from_csv + + + + +class TestBaseTransactionCosts: + @pytest.fixture(autouse=True) + def setup(self, client, application): + self.client = client + self.application = application + + self.header = { + "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyMTAyODc3OTUzNDg0MzUyNDI3IiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiYm54OW9WT1o4U3FJOTcyczBHYjd4dyIsIm5hbWUiOiJUZXN0IFVzZXIiLCJwaWN0dXJlIjoiIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJpYXQiOjE3MTM3MzQ0NjEsImV4cCI6OTk5OTk5OTk5OX0.eZYMQzcSRzkAq4Me8C6SNU3wduS7EIu_o5XGAbsDmU05GtyipQEb5iNJ1QiLg-11RbZFL3dvi8xKd3mpuw8b-5l6u8hwSpZg6wNPLY0zPX-EOwxeHLtev_2X5pUf1_IWAnso9K_knsK8CcmJoVsCyNNjlw3hrkChacJHGNzg0TTT1rh3oe6KCpbLvYlV6tUPfm5k3AMFZIT7Jntr38CZvs6gac6L_DhItJc3TNNUUHie2zgA29_r9YFlaEr_nGoSmBhIi-i0i0h34TL4JAb4qJkVM2YI2eTTv2HjEGtkx4mE5JvNQ0VxzHSJcCNOHh1gCiFD5c6rhvvxVeEqMkGGbCZKHX_vCgnIp0iE_OWyICjVTFPitQJ00fXLhyHyPb7q5J605tuK2iTHp2NCRJEXIAl9e0F_qASBBAfyL0C4FCBtvbnEMwtpoV1VWinkKgkI7JVH0AsyTugjXyAjxxsJxBTJT9qwZLxVBoaxgqNTOFfxvwstyq1VfCl3iBbpt71D" + } + + + with self.application.transaction_context() as ctx: + repo = ctx["transaction_repository"] + repo.delete_cascade(project_id="project-test") + + config = self.application['config'] + + transactions = read_transactions_with_prices_from_csv( + "test_transactions_tokens_cost_speed.csv", + config.PRICE_LIST_PATH + ) + for transaction in transactions: + repo.add(transaction) + + yield + + # Clean up test data + with self.application.transaction_context() as ctx: + repo = ctx["transaction_repository"] + repo.delete_cascade(project_id="project-test") + + def make_request(self, period, date_from, date_to, project_id="project-test"): + """Helper method to make API request for speed statistics.""" + + if date_from is None: + date_from = "" + if date_to is None: + date_to = "" + + api_url = f"/api/statistics/transactions_cost?" + #create api url add parameters if they are not empty + + if project_id != "": + api_url += f"project_id={project_id}" + + if period != "": + api_url += f"&period={period}" + if date_from != "": + api_url += f"&date_from={date_from}" + if date_to != "": + api_url += f"&date_to={date_to}" + + + return self.client.get( + api_url, + headers=self.header, + ) + + + + def extract_tokens_and_costs(self, response): + tokens = list( + [ + tuple( + [ + record["input_cumulative_total"] + record["output_cumulative_total"] + for record in date["records"] + ] + ) + for date in response.json() + ] + ) + + costs = list( + [ + tuple( + [record["total_cost"] for record in date["records"]] + ) + for date in response.json() + ] + ) + + # costs = list( + # [ + # tuple(map(lambda x: round(x * 1000000, 1), list(val))) + # for val in costs + # ] + # ) + + return tokens, costs + + def assert_response(self, response, expected_count, expected_tokens, expected_costs): + assert response.status_code == 200 + assert len(response.json()) == expected_count + + actual_tokens, actual_costs = self.extract_tokens_and_costs(response) + assert actual_tokens == expected_tokens , \ + f"Tokens are not equal: {actual_tokens} != {expected_tokens}" + assert len(actual_costs) == len(expected_costs) , \ + f"Costs has different length: {len(actual_costs)} != {len(expected_costs)}" + + for actual, expected in zip(actual_costs, expected_costs): + assert actual == pytest.approx(expected, rel=1e-4), \ + f"Costs are not equal: {actual} != {expected}" + + + +class TestTransactionCostErrors(TestBaseTransactionCosts): + def test_cost_statistics_for_not_existing_project(self): + """ + Tests token usage and cost statistics for not existing project. + + Given: A set of transactions within 2023-11-01 12:00-12:05 + When: Requesting token and cost statistics with 5-minute granularity + Then: Returns one interval with correct token counts and costs for each model + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:04:59", + "project-that-not-exists-xxx" + ) + + #current implementation returns 200 and empty list + assert response.status_code == 200 + assert len(response.json()) == 0 + + # TODO: this should be changed in the future + # assert response.status_code == 404 + # assert response.json() == {"error": "Project not found"} + + + def test_cost_statistics_for_not_wrong_period(self): + """ + Tests token usage and cost statistics for a not supported period. + + Given: Transactions spanning 30 minutes (2023-11-01 12:00-12:30) + When: Requesting token and cost statistics with not supported period + Then: Returns error + """ + response = self.make_request( + "not-existing-period", + "2023-11-01T12:00:00", + "2023-11-01T12:29:59" + ) + + response_data = response.json() + + # assert + response_data = response.json() + assert response.status_code == 422 + assert response_data['detail'][0]['input'] == 'not-existing-period' + + + def test_cost_statistics_for_date_from_after_date_to(self): + """ + Tests transaction cost statistics for a time frame when date_from is after date_to. + + Given: Transactions spanning -10 minutes (2023-11-01 12:10-12:00) date_from is after date_to + When: Requesting transaction cost statistics + Then: Returns error + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:10:00", + "2023-11-01T12:00:00" + ) + + # assert + response_data = response.json() + assert response.status_code == 400 + assert response_data['detail'] == "date_from cannot be after date_to" + + def test_cost_statistics_for_date_with_milliseconds(self): + """ + Tests transaction cost statistics for a time frame with milliseconds are not supported. + + Given: Transactions spanning 1 month (2023-11-01 12:00:00.000 - 2023-11-30T12:00:00.000) with milliseconds + When: Requesting transaction cost statistics + Then: Returns 400 error + """ + response = self.make_request( + "day", + "2023-11-01T12:00:00.000", + "2023-11-30T12:00:00.000" + ) + + # assert + response_data = response.json() + assert response.status_code == 400 + assert response_data['detail'] == "Invalid date format. Use YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS: Invalid date format" + + + +class TestMinutesGranularity(TestBaseTransactionCosts): + def test_5min_duration_with_5min_granularity_returns_single_interval_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 5-minute time frame with 5-minute granularity. + + Given: A set of transactions within 2023-11-01 12:00-12:05 + When: Requesting token and cost statistics with 5-minute granularity + Then: Returns one interval with correct token counts and costs for each model + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:04:59" + ) + + tokens_validation = [tuple([174])] + costs_validation = [tuple([0.0003155])] + + self.assert_response(response, 1, tokens_validation, costs_validation) + + def test_30min_duration_with_5min_granularity_returns_six_intervals_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 30-minute time frame with 5-minute granularity. + + Given: Transactions spanning 30 minutes (2023-11-01 12:00-12:30) + When: Requesting token and cost statistics with 5-minute granularity + Then: Returns 6 intervals with correct token counts and costs per model per interval + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:29:59" + ) + + tokens_validation = [ + tuple([0, 174, 0, 0]), + tuple([0, 730, 142, 332]), + tuple([0, 730, 142, 332]), + tuple([107, 730, 142, 332]), + tuple([107, 730, 1816, 332]), + tuple([419, 835, 1816, 497]), + ] + + costs_validation = [ + tuple([0, 0.0003155, 0, 0]), + tuple([0, 0.0013275, 0.0000568, 0.01092]), + tuple([0, 0.0013275, 0.0000568, 0.01092]), + tuple([0.001896, 0.0013275, 0.0000568, 0.01092]), + tuple([0.001896, 0.0013275, 0.0007264, 0.01092]), + tuple([0.008808, 0.001523, 0.0007264, 0.01887]), + ] + + self.assert_response(response, 6, tokens_validation, costs_validation) + + def test_1hour_duration_with_5min_granularity_returns_twelve_intervals_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 1-hour time frame with 5-minute granularity (period=5minutes). + + Given: Transactions spanning one hour (2023-11-01 12:00-13:00) + When: Requesting token and cost statistics with 5-minute granularity + Then: Returns 12 intervals with correct token counts and costs per model per interval + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:59:59" + ) + + tokens_validation = [ + tuple([0, 174, 0, 0]), + tuple([0, 730, 142, 332]), + tuple([0, 730, 142, 332]), + tuple([107, 730, 142, 332]), + tuple([107, 730, 1816, 332]), + tuple([419, 835, 1816, 497]), + tuple([419, 835, 1816, 497]), + tuple([419, 2847, 1816, 497]), + tuple([419, 3713, 1816, 497]), + tuple([419, 3713, 1816, 497]), + tuple([419, 7904, 1816, 497]), + tuple([419, 7904, 2778, 578]), + ] + + costs_validation = [ + tuple([0, 0.0003155, 0, 0]), + tuple([0, 0.0013275, 0.0000568, 0.01092]), + tuple([0, 0.0013275, 0.0000568, 0.01092]), + tuple([0.001896, 0.0013275, 0.0000568, 0.01092]), + tuple([0.001896, 0.0013275, 0.0007264, 0.01092]), + tuple([0.008808, 0.001523, 0.0007264, 0.01887]), + tuple([0.008808, 0.001523, 0.0007264, 0.01887]), + tuple([0.008808, 0.005541, 0.0007264, 0.01887]), + tuple([0.008808, 0.007212, 0.0007264, 0.01887]), + tuple([0.008808, 0.007212, 0.0007264, 0.01887]), + tuple([0.008808, 0.0151335, 0.0007264, 0.01887]), + tuple([0.008808, 0.0151335, 0.0011112, 0.02292]), + ] + + self.assert_response(response, 12, tokens_validation, costs_validation) + + def test_1month_duration_with_5min_granularity_returns_empty_list(self): + """ + Tests token usage and cost statistics for a 1 month time frame with 5-minute granularity (period=5minutes). + + Given: A time period with no recorded transactions (October 2023) + When: Requesting token and cost statistics with 5-minute granularity + Then: Returns an empty list + """ + response = self.make_request( + "5minutes", + "2023-10-01T12:00:00", + "2023-10-31T12:30:00" + ) + + self.assert_response(response, 0, [], []) + + +class TestHourlyGranularity(TestBaseTransactionCosts): + def test_30min_duration_with_hourly_granularity_returns_single_interval_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 30-minute time frame with hourly granularity (period=hour). + + Given: Transactions spanning 30 minutes (2023-11-01 12:00-12:30) + When: Requesting token and cost statistics with hourly granularity + Then: Returns one interval with aggregated token counts and costs per model + """ + response = self.make_request( + "hour", + "2023-11-01T12:00:00", + "2023-11-01T12:30:00" + ) + + tokens_validation = [tuple([419, 835, 1816, 497])] + costs_validation = [tuple([0.008808, 0.001523, 0.0007264, 0.01887])] + + self.assert_response(response, 1, tokens_validation, costs_validation) + + def test_1hour_duration_with_hourly_granularity_returns_two_intervals_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 1-hour time frame with hourly granularity (period=hour). + + Given: Transactions spanning one hour (2023-11-01 12:00-13:00) + When: Requesting token and cost statistics with hourly granularity + Then: Returns two intervals with aggregated token counts and costs per model + """ + response = self.make_request( + "hour", + "2023-11-01T12:00:00", + "2023-11-01T13:00:00" + ) + + tokens_validation = [tuple([419, 7904, 2778, 578]), tuple([419, 7904, 2778, 578])] + costs_validation = [ + tuple([0.008808, 0.0151335, 0.0011112, 0.02292]), + tuple([0.008808, 0.0151335, 0.0011112, 0.02292]), + ] + + self.assert_response(response, 2, tokens_validation, costs_validation) + + def test_24hour_duration_with_hourly_granularity_returns_24_intervals_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 24-hour time frame with hourly granularity (period=hour). + + Given: Transactions spanning 24 hours (2023-11-01 12:00 to 2023-11-02 12:00) + When: Requesting token and cost statistics with hourly granularity + Then: Returns 24 intervals with aggregated token counts and costs per model per hour + """ + response = self.make_request( + "hour", + "2023-11-01T12:00:00", + "2023-11-02T11:59:59" + ) + + tokens_validation = [ + tuple([419, 7904, 2778, 578]), + tuple([419, 7953, 2778, 578]), + tuple([419, 8045, 2778, 578]), + tuple([1231, 8045, 2778, 578]), + tuple([1231, 8045, 2778, 578]), + tuple([1468, 8045, 2778, 578]), + tuple([1468, 8045, 2778, 578]), + tuple([1468, 8045, 2778, 578]), + tuple([1781, 8079, 2778, 578]), + tuple([1781, 8079, 2778, 578]), + tuple([1781, 8079, 2778, 578]), + tuple([1781, 8079, 2778, 578]), + tuple([1781, 8079, 2778, 578]), + tuple([1781, 8079, 2778, 578]), + tuple([1781, 8079, 2778, 578]), + tuple([1781, 8079, 2778, 578]), + tuple([1781, 8079, 2787, 941]), + tuple([1781, 8079, 2787, 941]), + tuple([1781, 8079, 2787, 941]), + tuple([1781, 8079, 2787, 941]), + tuple([1871, 8079, 4429, 941]), + tuple([1871, 8079, 4429, 941]), + tuple([2253, 8079, 4429, 941]), + tuple([2253, 8079, 5337, 941]), + ] + + costs_validation = [ + tuple([0.008808, 0.0151335, 0.0011112, 0.02292]), + tuple([0.008808, 0.0152295, 0.0011112, 0.02292]), + tuple([0.008808, 0.01538, 0.0011112, 0.02292]), + tuple([0.02788, 0.01538, 0.0011112, 0.02292]), + tuple([0.02788, 0.01538, 0.0011112, 0.02292]), + tuple([0.03312, 0.01538, 0.0011112, 0.02292]), + tuple([0.03312, 0.01538, 0.0011112, 0.02292]), + tuple([0.03312, 0.01538, 0.0011112, 0.02292]), + tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), + tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), + tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), + tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), + tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), + tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), + tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), + tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), + tuple([0.040216, 0.0154425, 0.0011148, 0.04176]), + tuple([0.040216, 0.0154425, 0.0011148, 0.04176]), + tuple([0.040216, 0.0154425, 0.0011148, 0.04176]), + tuple([0.040216, 0.0154425, 0.0011148, 0.04176]), + tuple([0.041304, 0.0154425, 0.0017716, 0.04176]), + tuple([0.041304, 0.0154425, 0.0017716, 0.04176]), + tuple([0.04988, 0.0154425, 0.0017716, 0.04176]), + tuple([0.04988, 0.0154425, 0.0021348, 0.04176]), + ] + + self.assert_response(response, 24, tokens_validation, costs_validation) + + def test_1month_duration_with_hourly_granularity_returns_empty_list(self): + """ + Tests token usage and cost statistics for a 1 month time frame with no transactions with hourly granularity (period=hour). + + Given: A time period with no recorded transactions (2023-10-01 to 2023-10-31) + When: Requesting token and cost statistics with hourly granularity + Then: Returns an empty list + """ + response = self.make_request( + "hour", + "2023-10-01T12:00:00", + "2023-10-31T18:00:00" + ) + + self.assert_response(response, 0, [], []) + + +class TestDailyGranularity(TestBaseTransactionCosts): + def test_6hour_duration_with_daily_granularity_returns_single_interval_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 6-hour time frame with daily granularity (period=day). + + Given: Transactions spanning 6 hours (2023-11-01 12:00-18:00) + When: Requesting token and cost statistics with daily granularity + Then: Returns one interval with aggregated token counts and costs per model + """ + response = self.make_request( + "day", + "2023-11-01T12:00:00", + "2023-11-01T18:00:00" + ) + + tokens_validation = [tuple([1468, 8045, 2778, 578])] + costs_validation = [tuple([0.03312, 0.01538, 0.0011112, 0.02292])] + + self.assert_response(response, 1, tokens_validation, costs_validation) + + def test_24hour_duration_with_daily_granularity_returns_single_interval_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 24-hour time frame with daily granularity (period=day). + + Given: Transactions spanning 24 hours (2023-11-01 00:00-23:59), dates without time + When: Requesting token and cost statistics with daily granularity + Then: Returns one interval with aggregated token counts and costs per model + """ + response = self.make_request( + "day", + "2023-11-01", + "2023-11-01T23:59:59" + ) + + tokens_validation = [tuple([1781, 8079, 2778, 578])] + costs_validation = [tuple([0.040216, 0.0154425, 0.0011112, 0.02292])] + + self.assert_response(response, 1, tokens_validation, costs_validation) + + def test_1day_duration_with_daily_granularity_returns_two_intervals_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 1-day time frame with daily granularity (period=day). + + Given: Transactions spanning one day (2023-11-01 13:00 to 2023-11-02 13:00) + When: Requesting token and cost statistics with daily granularity + Then: Returns two intervals with aggregated token counts and costs per model + """ + response = self.make_request( + "day", + "2023-11-01T13:00:00", + "2023-11-02T13:00:00" + ) + + tokens_validation = [tuple([1362, 175, 0, 0]), tuple([2085, 175, 2559, 363])] + costs_validation = [ + tuple([0.031408, 0.000309, 0.0, 0.0]), + tuple([0.046632, 0.000309, 0.0010236, 0.01884]), + ] + + self.assert_response(response, 2, tokens_validation, costs_validation) + + def test_7day_duration_with_daily_granularity_returns_seven_intervals_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 7-day time frame with daily granularity (period=day). + + Given: Transactions spanning 7 days (2023-11-01 to 2023-11-07) + When: Requesting token and cost statistics with daily granularity + Then: Returns 7 intervals with aggregated token counts and costs per model per day + """ + response = self.make_request( + "day", + "2023-11-01T12:00:00", + "2023-11-07T11:59:59" + ) + + tokens_validation = [ + tuple([1781, 8079, 2778, 578]), + tuple([2805, 8079, 5337, 941]), + tuple([2805, 8079, 5337, 941]), + tuple([2805, 8079, 5337, 941]), + tuple([2805, 8079, 5337, 941]), + tuple([2902, 8079, 5337, 941]), + tuple([2902, 8079, 5337, 941]), + ] + + costs_validation = [ + tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), + tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), + tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), + tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), + tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), + tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), + tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), + ] + + self.assert_response(response, 7, tokens_validation, costs_validation) + + def test_1month_duration_with_daily_granularity_returns_30_intervals_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 1-month time frame with daily granularity (period=day). + + Given: Transactions spanning one month (2023-11-01 to 2023-11-30) + When: Requesting token and cost statistics with daily granularity + Then: Returns 30 intervals with aggregated token counts and costs per model per day + """ + response = self.make_request( + "day", + "2023-11-01T12:00:00", + "2023-11-30T12:00:00" + ) + + tokens_validation = [ + tuple([1781, 8079, 2778, 578]), + tuple([2805, 8079, 5337, 941]), + tuple([2805, 8079, 5337, 941]), + tuple([2805, 8079, 5337, 941]), + tuple([2805, 8079, 5337, 941]), + tuple([2902, 8079, 5337, 941]), + tuple([2902, 8079, 5337, 941]), + tuple([2902, 8079, 5337, 941]), + tuple([2902, 8079, 5337, 941]), + tuple([2902, 8079, 5337, 941]), + tuple([2902, 8079, 5337, 941]), + tuple([2902, 8079, 5337, 1390]), + tuple([2902, 8079, 6348, 2090]), + tuple([2902, 8079, 6348, 2090]), + tuple([2902, 8079, 6348, 2090]), + tuple([2902, 8079, 6348, 2090]), + tuple([2902, 8079, 6631, 2090]), + tuple([2902, 8079, 6631, 2090]), + tuple([2902, 8498, 6631, 2090]), + tuple([2902, 8643, 6631, 2090]), + tuple([2902, 8643, 6631, 2090]), + tuple([2902, 8643, 6631, 2090]), + tuple([2902, 8643, 6631, 2090]), + tuple([2902, 8643, 6631, 2090]), + tuple([2902, 8643, 6631, 2152]), + tuple([2902, 8643, 6631, 2152]), + tuple([2902, 8643, 6631, 2152]), + tuple([2902, 8643, 6631, 32357]), + tuple([2902, 8643, 6631, 32357]), + tuple([2902, 8643, 13616, 32357]), + ] + + costs_validation = [ + tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), + tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), + tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), + tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), + tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), + tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), + tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), + tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), + tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), + tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), + tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), + tuple([0.063408, 0.0154425, 0.0021348, 0.06414]), + tuple([0.063408, 0.0154425, 0.0025392, 0.10527]), + tuple([0.063408, 0.0154425, 0.0025392, 0.10527]), + tuple([0.063408, 0.0154425, 0.0025392, 0.10527]), + tuple([0.063408, 0.0154425, 0.0025392, 0.10527]), + tuple([0.063408, 0.0154425, 0.0026524, 0.10527]), + tuple([0.063408, 0.0154425, 0.0026524, 0.10527]), + tuple([0.063408, 0.0162695, 0.0026524, 0.10527]), + tuple([0.063408, 0.0165265, 0.0026524, 0.10527]), + tuple([0.063408, 0.0165265, 0.0026524, 0.10527]), + tuple([0.063408, 0.0165265, 0.0026524, 0.10527]), + tuple([0.063408, 0.0165265, 0.0026524, 0.10527]), + tuple([0.063408, 0.0165265, 0.0026524, 0.10527]), + tuple([0.063408, 0.0165265, 0.0026524, 0.108]), + tuple([0.063408, 0.0165265, 0.0026524, 0.108]), + tuple([0.063408, 0.0165265, 0.0026524, 0.108]), + tuple([0.063408, 0.0165265, 0.0026524, 1.9143]), + tuple([0.063408, 0.0165265, 0.0026524, 1.9143]), + tuple([0.063408, 0.0165265, 0.0054464, 1.9143]), + ] + + self.assert_response(response, 30, tokens_validation, costs_validation) + + def test_1day_duration_with_daily_granularity_returns_empty_list(self): + """ + Tests token usage and cost statistics for a 1 day time frame with no transactions with daily granularity (period=day). + + Given: A time period with no recorded transactions (2023-11-03 to 2023-11-04) + When: Requesting token and cost statistics with daily granularity + Then: Returns an empty list + """ + response = self.make_request( + "day", + "2023-11-03T12:00:00", + "2023-11-04T12:00:00" + ) + + self.assert_response(response, 0, [], []) + + +class TestWeeklyGranularity(TestBaseTransactionCosts): + def test_3day_duration_with_weekly_granularity_returns_single_interval_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 3-day time frame with weekly granularity (period=week). + + Given: Transactions spanning 3 days (2023-11-01 to 2023-11-03) + When: Requesting token and cost statistics with weekly granularity + Then: Returns one interval with aggregated token counts and costs per model + """ + response = self.make_request( + "week", + "2023-11-01T12:00:00", + "2023-11-03T12:00:00" + ) + + tokens_validation = [tuple([2805, 8079, 5337, 941])] + costs_validation = [tuple([0.062264, 0.0154425, 0.0021348, 0.04176])] + + self.assert_response(response, 1, tokens_validation, costs_validation) + + def test_7day_duration_with_weekly_granularity_returns_two_intervals_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 7-day time frame with weekly granularity (period=week). + + Given: Transactions spanning 7 days (2023-11-01 to 2023-11-07) + When: Requesting token and cost statistics with weekly granularity + Then: Returns two intervals with aggregated token counts and costs per model + """ + response = self.make_request( + "week", + "2023-11-01T12:00:00", + "2023-11-07T12:00:00" + ) + + tokens_validation = [tuple([2902, 8079, 5337, 941]), tuple([2902, 8079, 5337, 941])] + costs_validation = [ + tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), + tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), + ] + + self.assert_response(response, 2, tokens_validation, costs_validation) + + def test_2month_duration_with_weekly_granularity_returns_nine_intervals_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 2-month time frame with weekly granularity (period=week). + + Given: Transactions spanning 2 months (2023-11-01 to 2023-12-31) + When: Requesting token and cost statistics with weekly granularity + Then: Returns 9 intervals with aggregated token counts and costs per model per week + """ + response = self.make_request( + "week", + "2023-11-01T12:00:00", + "2023-12-31T00:00:00" + ) + + tokens_validation = [ + tuple([2902, 8079, 5337, 941]), + tuple([2902, 8079, 6348, 2090]), + tuple([2902, 8643, 6631, 2090]), + tuple([2902, 8643, 6631, 2152]), + tuple([2902, 8643, 13730, 33279]), + tuple([3877, 8859, 13730, 33279]), + tuple([4682, 9159, 13730, 33279]), + tuple([4682, 9159, 13730, 33279]), + tuple([4682, 9159, 13730, 33279]), + ] + + costs_validation = [ + tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), + tuple([0.063408, 0.0154425, 0.0025392, 0.10527]), + tuple([0.063408, 0.0165265, 0.0026524, 0.10527]), + tuple([0.063408, 0.0165265, 0.0026524, 0.108]), + tuple([0.063408, 0.0165265, 0.005492, 1.96632]), + tuple([0.085768, 0.016896, 0.005492, 1.96632]), + tuple([0.104496, 0.0174505, 0.005492, 1.96632]), + tuple([0.104496, 0.0174505, 0.005492, 1.96632]), + tuple([0.104496, 0.0174505, 0.005492, 1.96632]), + ] + + self.assert_response(response, 9, tokens_validation, costs_validation) + + def test_2week_duration_with_weekly_granularity_returns_empty_list(self): + """ + Tests token usage and cost statistics for a 2-week time frame with no transactions with weekly granularity (period=week). + + Given: A time period with no recorded transactions (2024-03-29 to 2024-04-12) + When: Requesting token and cost statistics with weekly granularity + Then: Returns an empty list + """ + response = self.make_request( + "week", + "2024-03-29T00:00:00", + "2024-04-12T00:00:00" + ) + + self.assert_response(response, 0, [], []) + + +class TestMonthlyGranularity(TestBaseTransactionCosts): + def test_7day_duration_with_monthly_granularity_returns_single_interval_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 7-day time frame with monthly granularity (period=month). + + Given: Transactions spanning 7 days (2023-11-01 to 2023-11-07) + When: Requesting token and cost statistics with monthly granularity + Then: Returns one interval with aggregated token counts and costs per model + """ + response = self.make_request( + "month", + "2023-11-01T12:00:00", + "2023-11-07T12:00:00" + ) + + tokens_validation = [tuple([2902, 8079, 5337, 941])] + costs_validation = [tuple([0.063408, 0.0154425, 0.0021348, 0.04176])] + + self.assert_response(response, 1, tokens_validation, costs_validation) + + def test_1month_duration_with_monthly_granularity_returns_single_interval_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 1-month time frame with monthly granularity (period=month). + + Given: Transactions spanning one month (2023-11-01 to 2023-11-30) + When: Requesting token and cost statistics with monthly granularity + Then: Returns one interval with aggregated token counts and costs per model + """ + response = self.make_request( + "month", + "2023-11-01", + "2023-11-30T23:59:59" + ) + + tokens_validation = [tuple([2902, 8643, 13616, 32357])] + costs_validation = [tuple([0.063408, 0.0165265, 0.0054464, 1.9143])] + + self.assert_response(response, 1, tokens_validation, costs_validation) + + def test_1month_duration_with_monthly_granularity_day_to_without_endof_day_time(self): + """ + Tests token usage and cost statistics for a 1-month time frame with monthly granularity (period=month), making two requests where one is without end of day time. + + Given: Transactions spanning one month (2023-11-01 to 2023-11-30) one request is without end of day time, another is with (23:59:59) + When: Requesting two times to token and cost statistics with monthly granularity + Then: Returns one interval but different values for requested time frames + """ + + response_0 = self.make_request( + "month", + "2023-11-01", + "2023-11-30" + ) + + response_1 = self.make_request( + "month", + "2023-11-01", + "2023-11-30T23:59:59" + ) + + resp_data_0 = response_0.json() + resp_data_1 = response_1.json() + + + expected_tokens_0 = [tuple([2902, 8643, 6631, 32357])] + expected_costs_0 = [tuple([0.063408, 0.0165265, 0.0026524, 1.9143])] + + expected_tokens_1 = [tuple([2902, 8643, 13616, 32357])] + expected_costs_1 = [tuple([0.063408, 0.0165265, 0.0054464, 1.9143])] + + # pytest assert not equal response_0 and response_1 + assert resp_data_0 != resp_data_1 + self.assert_response(response_0, 1, expected_tokens_0, expected_costs_0) + self.assert_response(response_1, 1, expected_tokens_1, expected_costs_1) + + def test_6month_duration_with_monthly_granularity_returns_six_intervals_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 6-month time frame with monthly granularity (period=month). + + Given: Transactions spanning 6 months (2023-10-01 to 2024-03-31) + When: Requesting token and cost statistics with monthly granularity + Then: Returns 6 intervals with aggregated token counts and costs per model per month + """ + response = self.make_request( + "month", + "2023-10-01", + "2024-03-31" + ) + + tokens_validation = [ + tuple([0, 0, 0, 0]), + tuple([2902, 8643, 13616, 32357]), + tuple([4682, 9384, 13730, 33279]), + tuple([4975, 9804, 14278, 34344]), + tuple([4975, 9804, 14278, 34344]), + tuple([5586, 9804, 14278, 61424]), + ] + + costs_validation = [ + tuple([0.0, 0.0, 0.0, 0.0]), + tuple([0.063408, 0.0165265, 0.0054464, 1.9143]), + tuple([0.104496, 0.0178535, 0.005492, 1.96632]), + tuple([0.11116, 0.0186715, 0.0057112, 2.02788]), + tuple([0.11116, 0.0186715, 0.0057112, 2.02788]), + tuple([0.123792, 0.0186715, 0.0057112, 3.5808]), + ] + + self.assert_response(response, 6, tokens_validation, costs_validation) + + def test_1month_duration_with_monthly_granularity_returns_empty_list(self): + """ + Tests token usage and cost statistics for a 1 month time frame with no transactions with monthly granularity (period=month). + + Given: A time period with no recorded transactions (2024-05-01 to 2024-06-01) + When: Requesting token and cost statistics with monthly granularity + Then: Returns an empty list + """ + response = self.make_request( + "month", + "2024-05-01", + "2024-06-01" + ) + + self.assert_response(response, 0, [], []) + + +class TestYearlyGranularity(TestBaseTransactionCosts): + def test_2month_duration_with_yearly_granularity_returns_single_interval_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 2-month time frame with yearly granularity (period=year). + + Given: Transactions spanning 2 months (2023-11-01 to 2023-12-31) + When: Requesting token and cost statistics with yearly granularity + Then: Returns one interval with aggregated token counts and costs per model + """ + response = self.make_request( + "year", + "2023-11-01", + "2023-12-31T23:59:59" + ) + + tokens_validation = [tuple([4682, 9245, 13730, 33279])] + costs_validation = [tuple([0.104496, 0.017596, 0.005492, 1.96632])] + + self.assert_response(response, 1, tokens_validation, costs_validation) + + def test_2month_duration_with_yearly_granularity_day_to_without_endof_day_time(self): + """ + Tests token usage and cost statistics for a 2-month time frame with yearly granularity (period=year), making two requests where one is without end of day time. + + Given: Transactions spanning one month (2023-11-01 to 2023-11-30) one request is without end of day time, another is with (23:59:59) + When: Requesting two times to token and cost statistics with monthly granularity + Then: Returns one interval but different values for requested time frames + """ + + response_0 = self.make_request( + "year", + "2023-11-01", + "2023-12-31" + ) + + response_1 = self.make_request( + "year", + "2023-11-01", + "2023-12-31T23:59:59" + ) + + resp_data_0 = response_0.json() + resp_data_1 = response_1.json() + + + expected_tokens_0 = [tuple([4682, 9159, 13730, 33279])] + expected_costs_0 = [tuple([0.104496, 0.0174505, 0.005492, 1.96632])] + + expected_tokens_1 = [tuple([4682, 9245, 13730, 33279])] + expected_costs_1 = [tuple([0.104496, 0.017596, 0.005492, 1.96632])] + + # pytest assert not equal response_0 and response_1 + assert resp_data_0 != resp_data_1 + self.assert_response(response_0, 1, expected_tokens_0, expected_costs_0) + self.assert_response(response_1, 1, expected_tokens_1, expected_costs_1) + + + def test_5month_duration_with_yearly_granularity_returns_two_intervals_with_tokens_and_costs(self): + """ + Tests token usage and cost statistics for a 5-month time frame with yearly granularity (period=year). + + Given: Transactions spanning from 2023 to 2024 (2023-11-01 to 2024-03-31) + When: Requesting token and cost statistics with yearly granularity + Then: Returns two intervals with aggregated token counts and costs per model per year + """ + response = self.make_request( + "year", + "2023-11-01", + "2024-03-31" + ) + + tokens_validation = [ + tuple([4682, 9384, 13730, 33279]), + tuple([5586, 9804, 14278, 61424]), + ] + + costs_validation = [ + tuple([0.104496, 0.0178535, 0.005492, 1.96632]), + tuple([0.123792, 0.0186715, 0.0057112, 3.5808]), + ] + + self.assert_response(response, 2, tokens_validation, costs_validation) + + def test_6month_duration_with_yearly_granularity_returns_empty_list(self): + """ + Tests token usage and cost statistics for a 6-month time frame with no transactions with yearly granularity (period=year). + + Given: A time period with no recorded transactions (2024-05-31 to 2024-12-31) + When: Requesting token and cost statistics with yearly granularity + Then: Returns an empty list + """ + response = self.make_request( + "year", + "2024-05-31", + "2024-12-31" + ) + + self.assert_response(response, 0, [], []) \ No newline at end of file diff --git a/backend/tests/test_api_statistics_transaction_counts.py b/backend/tests/test_api_statistics_transaction_counts.py new file mode 100644 index 00000000..189e0b22 --- /dev/null +++ b/backend/tests/test_api_statistics_transaction_counts.py @@ -0,0 +1,728 @@ +import pytest +from test_utils import read_transactions_from_csv + +class TestBaseTransactionCounts: + """Base test class for transaction count statistics tests.""" + + @pytest.fixture(autouse=True) + def setup(self, client, application): + """Setup test data and common test fixtures.""" + self.client = client + self.application = application + self.header = { + "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyMTAyODc3OTUzNDg0MzUyNDI3IiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiYm54OW9WT1o4U3FJOTcyczBHYjd4dyIsIm5hbWUiOiJUZXN0IFVzZXIiLCJwaWN0dXJlIjoiIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJpYXQiOjE3MTM3MzQ0NjEsImV4cCI6OTk5OTk5OTk5OX0.eZYMQzcSRzkAq4Me8C6SNU3wduS7EIu_o5XGAbsDmU05GtyipQEb5iNJ1QiLg-11RbZFL3dvi8xKd3mpuw8b-5l6u8hwSpZg6wNPLY0zPX-EOwxeHLtev_2X5pUf1_IWAnso9K_knsK8CcmJoVsCyNNjlw3hrkChacJHGNzg0TTT1rh3oe6KCpbLvYlV6tUPfm5k3AMFZIT7Jntr38CZvs6gac6L_DhItJc3TNNUUHie2zgA29_r9YFlaEr_nGoSmBhIi-i0i0h34TL4JAb4qJkVM2YI2eTTv2HjEGtkx4mE5JvNQ0VxzHSJcCNOHh1gCiFD5c6rhvvxVeEqMkGGbCZKHX_vCgnIp0iE_OWyICjVTFPitQJ00fXLhyHyPb7q5J605tuK2iTHp2NCRJEXIAl9e0F_qASBBAfyL0C4FCBtvbnEMwtpoV1VWinkKgkI7JVH0AsyTugjXyAjxxsJxBTJT9qwZLxVBoaxgqNTOFfxvwstyq1VfCl3iBbpt71D" + } + + + + # Load test data + transactions = read_transactions_from_csv("test_transactions.csv") + + # Add transactions to the database + with self.application.transaction_context() as ctx: + repo = ctx["transaction_repository"] + for transaction in transactions: + repo.add(transaction) + + yield + + # Delete transactions from the database + with self.application.transaction_context() as ctx: + repo = ctx["transaction_repository"] + repo.delete_cascade("project-test") + + + + def _assert_response_status_and_length(self, response, expected_status, expected_length): + """Helper method to check response status and length.""" + assert response.status_code == expected_status + assert len(response.json()) == expected_length + + def _assert_status_counts(self, response, expected_counts): + """Helper method to verify status counts in response.""" + actual_counts = [ + (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) + for stat in response.json() + ] + assert actual_counts == expected_counts + + + def make_request(self, period, date_from, date_to, project_id="project-test"): + """Helper method to make API request for count statistics.""" + + if date_from is None: + date_from = "" + if date_to is None: + date_to = "" + + api_url = f"/api/statistics/transactions_count?" + #create api url add parameters if they are not empty + + if project_id != "": + api_url += f"project_id={project_id}" + + if period != "": + api_url += f"&period={period}" + if date_from != "": + api_url += f"&date_from={date_from}" + if date_to != "": + api_url += f"&date_to={date_to}" + + + return self.client.get( + api_url, + headers=self.header, + ) + + + +class TestTransactionCountsErrors(TestBaseTransactionCounts): + def test_count_statistics_for_not_existing_project(self): + """ + Tests transaction count statistics for not existing project. + + Given: A set of transactions within 2023-11-01 12:00-12:05 + When: Requesting transaction count statistics with 5-minute granularity + Then: Returns one interval with correct transaction counts + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:04:59", + "project-that-not-exists-xxx" + ) + + #current implementation returns 200 and empty list + assert response.status_code == 200 + assert len(response.json()) == 0 + + # TODO: this should be changed in the future + # assert response.status_code == 404 + # assert response.json() == {"error": "Project not found"} + + + def test_count_statistics_for_not_wrong_period(self): + """ + Tests transaction count statistics for a not supported period. + + Given: Transactions spanning 30 minutes (2023-11-01 12:00-12:30) + When: Requesting transaction count statistics with not supported period + Then: Returns error + """ + response = self.make_request( + "not-existing-period", + "2023-11-01T12:00:00", + "2023-11-01T12:29:59" + ) + + response_data = response.json() + + # assert + response_data = response.json() + assert response.status_code == 422 + assert response_data['detail'][0]['input'] == 'not-existing-period' + + + def test_count_statistics_for_date_from_after_date_to(self): + """ + Tests transaction count statistics for a time frame when date_from is after date_to. + + Given: Transactions spanning -10 minutes (2023-11-01 12:10-12:00) date_from is after date_to + When: Requesting transaction cost statistics + Then: Returns error + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:10:00", + "2023-11-01T12:00:00" + ) + + # assert + response_data = response.json() + assert response.status_code == 400 + assert response_data['detail'] == "date_from cannot be after date_to" + + def test_count_statistics_for_date_with_milliseconds(self): + """ + Tests transaction count statistics for a time frame with milliseconds are not supported. + + Given: Transactions spanning 1 month (2023-11-01 12:00:00.000 - 2023-11-30T12:00:00.000) with milliseconds + When: Requesting transaction count statistics + Then: Returns 400 error + """ + response = self.make_request( + "day", + "2023-11-01T12:00:00.000", + "2023-11-30T12:00:00.000" + ) + + # assert + response_data = response.json() + assert response.status_code == 400 + assert response_data['detail'] == "Invalid date format. Use YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS: Invalid date format" + + def test_count_statistics_for_date_without_minutes(self): + """ + Tests transaction count statistics for a time frame without minutes are not supported. + + Given: Transactions spanning 1 month (2023-11-01 12:00:00 - 2023-11-30T12:00:00) without minutes + When: Requesting transaction count statistics + Then: Returns 400 error + """ + response = self.make_request( + "day", + "2023-11-01T12:00", + "2023-11-30T12:00" + ) + + # assert + response_data = response.json() + assert response.status_code == 400 + assert response_data['detail'] == "Invalid date format. Use YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS: Invalid date format" + +class Test5MinuteTransactionCounts(TestBaseTransactionCounts): + """Tests for 5-minute granularity transaction counts.""" + + def test_5min_duration_with_5min_granularity_returns_correct_status_counts(self): + """ + Tests transaction statistics for a 5-minute interval. + + Given: A set of transactions within 2023-11-01 12:00-12:05 + When: Requesting transaction counts with 5-minute granularity + Then: Returns one interval with correct HTTP status code counts (200,300,400,500) + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:04:59" + ) + + self._assert_response_status_and_length(response, 200, 1) + self._assert_status_counts(response, [(3, 1, 2, 0)]) + + def test_30min_duration_with_5min_granularity_returns_six_5min_intervals(self): + """ + Tests transaction statistics for a 30-minute period with 5-minute granularity. + + Given: Transactions spanning 30 minutes (2023-11-01 12:00-12:30) + When: Requesting transaction counts with 5-minute granularity + Then: Returns 6 intervals with correct status counts per interval + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:29:59" + ) + + self._assert_response_status_and_length(response, 200, 6) + self._assert_status_counts(response, [ + (3, 1, 2, 0), + (2, 0, 0, 0), + (0, 0, 0, 0), + (1, 0, 1, 0), + (2, 1, 0, 0), + (3, 0, 0, 2), + ]) + + def test_1hour_duration_with_5min_granularity_returns_twelve_5min_intervals(self): + """ + Tests transaction statistics for a 1-hour duration with 5-minute granularity. + + Given: Transactions spanning one hour (2023-11-01 12:00-13:00) + When: Requesting transaction counts with 5-minute granularity + Then: Returns 12 intervals with correct status counts per interval + """ + response = self.make_request( + "5minutes", + "2023-11-01T12:00:00", + "2023-11-01T12:59:59" + ) + + self._assert_response_status_and_length(response, 200, 12) + self._assert_status_counts(response, [ + (3, 1, 2, 0), + (2, 0, 0, 0), + (0, 0, 0, 0), + (1, 0, 1, 0), + (2, 1, 0, 0), + (3, 0, 0, 2), + (0, 0, 0, 0), + (1, 0, 0, 0), + (2, 0, 0, 0), + (0, 0, 0, 0), + (5, 0, 2, 0), + (4, 1, 2, 0), + ]) + + def test_1month_duration_with_5min_granularity_returns_empty_list(self): + """ + Tests transaction statistics for a 1-month duration with 5-minute granularity. + + Given: A time period with no recorded transactions + When: Requesting transaction counts for October 2023 + Then: Returns an empty list + """ + response = self.make_request( + "5minutes", + "2023-10-01T00:00:00", + "2023-10-31T00:00:00" + ) + + self._assert_response_status_and_length(response, 200, 0) + + +class TestHourlyTransactionCounts(TestBaseTransactionCounts): + """Tests for hourly granularity transaction counts.""" + + def test_30min_duration_with_hourly_granularity_returns_single_interval(self): + """ + Tests counting the transactions in a 30 minute duration with hourly granularity. + + Given: Transactions within 30 minutes (2023-11-01 12:00-12:30) + When: Requesting transaction counts with hourly granularity + Then: Returns one interval with aggregated status counts + """ + response = self.make_request( + "hour", + "2023-11-01T12:00:00", + "2023-11-01T12:30:00" + ) + + self._assert_response_status_and_length(response, 200, 1) + self._assert_status_counts(response, [(11, 2, 3, 2)]) + + def test_1hour_duration_with_hourly_granularity_returns_two_intervals(self): + """ + Tests counting the transactions in a 1-hour duration (from 12:00 to 13:00) with hourly granularity (period=hour). + + Given: Transactions spanning one hour + When: Requesting hourly transaction counts + Then: Returns two intervals with correct aggregated counts + """ + response = self.make_request( + "hour", + "2023-11-01T12:00:00", + "2023-11-01T13:00:00" + ) + + self._assert_response_status_and_length(response, 200, 2) + self._assert_status_counts(response, [(23, 3, 7, 2), (0, 0, 0, 0)]) + + def test_24hour_duration_with_hourly_granularity_returns_24_intervals(self): + """ + Tests counting the transactions in a 24-hour duration (from 12:00 to 12:00 the next day) with hourly granularity (period=hour). + + Given: Transactions spanning 24 hours + When: Requesting hourly transaction counts + Then: Returns 24 intervals with correct status counts per hour + """ + response = self.make_request( + "hour", + "2023-11-01T12:00:00", + "2023-11-02T11:59:59" + ) + + self._assert_response_status_and_length(response, 200, 24) + self._assert_status_counts(response, [ + (23, 3, 7, 2), + (1, 0, 0, 0), + (1, 0, 2, 0), + (1, 1, 0, 0), + (0, 0, 0, 0), + (1, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (2, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (2, 0, 0, 2), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (4, 0, 0, 0), + (0, 0, 0, 0), + (1, 0, 0, 0), + (1, 0, 0, 0), + ]) + + def test_1month_duration_with_hourly_granularity_returns_empty_list(self): + """ + Tests counting the transactions in a 1-month duration (from 2023-10-01 to 2023-10-31) with hourly granularity (period=hour). + + Given: A month period without transactions (October 2023) + When: Requesting hourly transaction counts + Then: Returns empty list + """ + response = self.make_request( + "hour", + "2023-10-01T00:00:00", + "2023-10-31T00:00:00" + ) + + self._assert_response_status_and_length(response, 200, 0) + + +class TestDailyTransactionCounts(TestBaseTransactionCounts): + """Tests for daily granularity transaction counts.""" + + def test_6hour_duration_with_daily_granularity_returns_single_interval(self): + """ + Tests counting the transactions in a 6-hour duration (from 12:00 to 18:00) with daily granularity (period=day). + + Given: Transactions spanning 6 hours + When: Requesting daily transaction counts + Then: Returns one interval with aggregated status counts + """ + response = self.make_request( + "day", + "2023-11-01T12:00:00", + "2023-11-01T18:00:00" + ) + + self._assert_response_status_and_length(response, 200, 1) + self._assert_status_counts(response, [(27, 4, 9, 2)]) + + def test_1day_duration_with_daily_granularity_returns_single_interval(self): + """ + Tests counting the transactions in a 1-day duration (from 2023-11-01 to 2023-11-02) with daily granularity (period=day). + + Given: Transactions spanning 1 day + When: Requesting daily transaction counts + Then: Returns one interval with aggregated status counts + """ + response = self.make_request( + "day", + "2023-11-01T00:00:00", + "2023-11-01T23:59:59" + ) + + self._assert_response_status_and_length(response, 200, 1) + self._assert_status_counts(response, [(29, 4, 9, 2)]) + + def test_1day_duration_with_daily_granularity_same_date_returns_empty_list(self): + """ + Tests counting the transactions in a 1-day duration (from 2023-11-01 to 2023-11-01) with daily granularity (period=day). + + Given: Transactions spanning 1 day + When: Requesting daily transaction counts + Then: Returns one interval with aggregated status counts + """ + response = self.make_request( + "day", + "2023-11-01T00:00:00", + "2023-11-01T00:00:00" + ) + + self._assert_response_status_and_length(response, 200, 0) + + + + def test_24hour_duration_with_daily_granularity_returns_two_intervals(self): + """ + Tests counting the transactions in a 24-hour duration (from 2023-11-01 to 2023-11-02) with daily granularity (period=day). + + Given: Transactions spanning 2 days + When: Requesting daily transaction counts + Then: Returns two intervals with aggregated status counts + """ + response = self.make_request( + "day", + "2023-11-01T13:00:00", + "2023-11-02T13:00:00" + ) + + self._assert_response_status_and_length(response, 200, 2) + self._assert_status_counts(response, [(6, 1, 2, 0), (9, 0, 0, 2)]) + + def test_7day_duration_with_daily_granularity_returns_seven_intervals(self): + """ + Tests counting the transactions in a 7-day duration (from 2023-11-01 to 2023-11-07) with daily granularity (period=day). + + Given: Transactions spanning 7 days + When: Requesting daily transaction counts + Then: Returns 7 intervals with aggregated status counts + """ + response = self.make_request( + "day", + "2023-11-01T12:00:00", + "2023-11-07T11:59:59" + ) + + self._assert_response_status_and_length(response, 200, 7) + self._assert_status_counts(response, [ + (29, 4, 9, 2), + (10, 0, 0, 2), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (1, 1, 1, 0), + (0, 0, 0, 0), + ]) + + def test_30day_duration_with_daily_granularity_returns_thirty_intervals(self): + """ + Tests counting the transactions in a 30-day duration (from 2023-11-01 to 2023-11-30) with daily granularity (period=day). + + Given: Transactions spanning 30 days + When: Requesting daily transaction counts + Then: Returns 30 intervals with aggregated status counts + """ + response = self.make_request( + "day", + "2023-11-01T12:00:00", + "2023-11-30T12:00:00" + ) + + self._assert_response_status_and_length(response, 200, 30) + self._assert_status_counts(response, [ + (29, 4, 9, 2), + (10, 0, 0, 2), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (1, 1, 1, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (3, 0, 0, 0), + (1, 0, 0, 0), + (0, 0, 2, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (1, 0, 0, 0), + (0, 0, 0, 0), + (1, 0, 0, 0), + (1, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (0, 0, 0, 0), + (1, 0, 0, 1), + (0, 0, 0, 0), + (0, 0, 0, 0), + (1, 0, 0, 0), + (0, 0, 0, 0), + (1, 0, 0, 0), + ]) + + +class TestWeeklyTransactionCounts(TestBaseTransactionCounts): + """Tests for weekly granularity transaction counts.""" + + def test_2month_duration_with_weekly_granularity_returns_nine_intervals(self): + """ + Tests counting the transactions by status code in a 2-month duration (from 2023-11-01 to 2023-12-31) with weekly granularity (period=week). + + Given: Transactions spanning 2 months + When: Requesting weekly transaction counts + Then: Returns 9 intervals with aggregated status counts + """ + response = self.make_request( + "week", + "2023-11-01T12:00:00", + "2023-12-31T00:00:00" + ) + + self._assert_response_status_and_length(response, 200, 9) + self._assert_status_counts(response, [ + (40, 5, 10, 4), + (4, 0, 0, 0), + (3, 0, 2, 0), + (1, 0, 0, 1), + (5, 1, 0, 0), + (3, 0, 0, 0), + (2, 0, 0, 0), + (0, 3, 0, 0), + (0, 0, 0, 0), + ]) + + def test_3day_duration_with_weekly_granularity_returns_single_interval(self): + """ + Tests counting the transactions by status code in a 3-day duration (from 2023-11-01 to 2023-11-03) with weekly granularity (period=week). + + Given: Transactions spanning 3 days + When: Requesting weekly transaction counts + Then: Returns 1 interval with aggregated status counts + """ + response = self.make_request( + "week", + "2023-11-01T12:00:00", + "2023-11-03T12:00:00" + ) + + self._assert_response_status_and_length(response, 200, 1) + self._assert_status_counts(response, [(39, 4, 9, 4)]) + + def test_7day_duration_with_weekly_granularity_returns_two_intervals(self): + """ + Tests counting the transactions by status code in a 7-day duration (from 2023-11-01 to 2023-11-07) with weekly granularity (period=week). + + Given: Transactions spanning 7 days + When: Requesting weekly transaction counts + Then: Returns 2 intervals with aggregated status counts + """ + response = self.make_request( + "week", + "2023-11-01T12:00:00", + "2023-11-07T12:00:00" + ) + + self._assert_response_status_and_length(response, 200, 2) + self._assert_status_counts(response, [(40, 5, 10, 4), (0, 0, 0, 0)]) + + def test_1month_duration_with_weekly_granularity_returns_single_interval(self): + """ + Tests counting the transactions by status code in a 1-month duration (from 2024-03-29 to 2024-04-30) with weekly granularity (period=week). + + Given: A future time period without transactions + When: Requesting weekly transaction counts + Then: Returns empty list + """ + response = self.make_request( + "week", + "2024-03-29T00:00:00", + "2024-04-30T00:00:00" + ) + + self._assert_response_status_and_length(response, 200, 0) + + +class TestMonthlyTransactionCounts(TestBaseTransactionCounts): + """Tests for monthly granularity transaction counts.""" + + def test_7day_duration_with_monthly_granularity_returns_single_interval(self): + """ + Tests counting the transactions by status code in a 7-day duration (from 2023-11-01 to 2023-11-07) with monthly granularity (period=month). + + Given: Transactions spanning 7 days + When: Requesting monthly transaction counts + Then: Returns 1 interval with aggregated status counts + """ + response = self.make_request( + "month", + "2023-11-01T12:00:00", + "2023-11-07T12:00:00" + ) + + # assert + self._assert_response_status_and_length(response, 200, 1) + self._assert_status_counts(response, [(40, 5, 10, 4)]) + + def test_30day_duration_with_monthly_granularity_returns_single_interval(self): + """ + Tests counting the transactions by status code in a 30-day duration (from 2023-11-01 to 2023-11-30) with monthly granularity (period=month). + + Given: Transactions spanning 30 days + When: Requesting monthly transaction counts + Then: Returns 1 interval with aggregated status counts + """ + response = self.make_request( + "month", + "2023-11-01T12:00:00", + "2023-11-30T12:00:00" + ) + + self._assert_response_status_and_length(response, 200, 1) + self._assert_status_counts(response, [(50, 5, 12, 5)]) + + def test_6month_duration_with_monthly_granularity_returns_six_intervals(self): + """ + Tests counting the transactions by status code in a 6-month duration (from 2023-10-01 to 2024-03-31) with monthly granularity (period=month). + + Given: Transactions spanning 6 months + When: Requesting monthly transaction counts + Then: Returns 6 intervals with aggregated status counts + """ + response = self.make_request( + "month", + "2023-10-01T12:00:00", + "2024-03-31T12:00:00" + ) + + self._assert_response_status_and_length(response, 200, 6) + self._assert_status_counts(response, [ + (0, 0, 0, 0), + (50, 5, 12, 5), + (10, 4, 0, 0), + (6, 1, 1, 1), + (0, 0, 0, 0), + (7, 4, 4, 1), + ]) + + def test_1month_duration_with_monthly_granularity_returns_empty_list(self): + """ + Tests counting the transactions by status code in a 1-month duration (from 2024-05-01 to 2024-05-31) with monthly granularity (period=month). + + Given: A month period without transactions (May 2024) + When: Requesting monthly transaction counts + Then: Returns empty list + """ + response = self.make_request( + "month", + "2024-05-01T12:00:00", + "2024-05-31T12:00:00" + ) + + self._assert_response_status_and_length(response, 200, 0) + + +class TestYearlyTransactionCounts(TestBaseTransactionCounts): + """Tests for yearly granularity transaction counts.""" + + def test_2month_duration_with_yearly_granularity_returns_single_interval(self): + """ + Tests counting the transactions by status code in a 2-month duration (from 2023-11-01 to 2023-12-31) with yearly granularity (period=year). + + Given: Transactions spanning 2 months in 2023 + When: Requesting yearly transaction counts + Then: Returns one interval with aggregated status counts + """ + response = self.make_request( + "year", + "2023-11-01T12:00:00", + "2023-12-31T12:00:00" + ) + + self._assert_response_status_and_length(response, 200, 1) + self._assert_status_counts(response, [(59, 9, 12, 5)]) + + def test_5month_duration_with_yearly_granularity_returns_two_intervals(self): + """ + Tests counting the transactions by status code in a 5-month duration (from 2023-11-01 to 2024-03-31) with yearly granularity (period=year). + + Given: Transactions spanning 5 months across 2023-2024 + When: Requesting yearly transaction counts + Then: Returns two intervals with aggregated status counts + """ + response = self.make_request( + "year", + "2023-11-01T12:00:00", + "2024-03-31T12:00:00" + ) + + self._assert_response_status_and_length(response, 200, 2) + self._assert_status_counts(response, [(60, 9, 12, 5), (13, 5, 5, 2)]) + + def test_9month_duration_with_yearly_granularity_returns_empty_interval(self): + """ + Tests counting the transactions by status code in a 9-month duration (from 2024-03-31 to 2024-12-31) with yearly granularity (period=year). + + Given: A future time period without transactions + When: Requesting yearly transaction counts + Then: Returns empty list + """ + response = self.make_request( + "year", + "2024-03-31T12:00:00", + "2024-12-31T12:00:00" + ) + + self._assert_response_status_and_length(response, 200, 0) + diff --git a/backend/tests/test_count_statistics.py b/backend/tests/test_count_statistics.py deleted file mode 100644 index c442cd10..00000000 --- a/backend/tests/test_count_statistics.py +++ /dev/null @@ -1,638 +0,0 @@ -from utils import read_transactions_from_csv - -header = { - "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyMTAyODc3OTUzNDg0MzUyNDI3IiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiYm54OW9WT1o4U3FJOTcyczBHYjd4dyIsIm5hbWUiOiJUZXN0IFVzZXIiLCJwaWN0dXJlIjoiIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJpYXQiOjE3MTM3MzQ0NjEsImV4cCI6OTk5OTk5OTk5OX0.eZYMQzcSRzkAq4Me8C6SNU3wduS7EIu_o5XGAbsDmU05GtyipQEb5iNJ1QiLg-11RbZFL3dvi8xKd3mpuw8b-5l6u8hwSpZg6wNPLY0zPX-EOwxeHLtev_2X5pUf1_IWAnso9K_knsK8CcmJoVsCyNNjlw3hrkChacJHGNzg0TTT1rh3oe6KCpbLvYlV6tUPfm5k3AMFZIT7Jntr38CZvs6gac6L_DhItJc3TNNUUHie2zgA29_r9YFlaEr_nGoSmBhIi-i0i0h34TL4JAb4qJkVM2YI2eTTv2HjEGtkx4mE5JvNQ0VxzHSJcCNOHh1gCiFD5c6rhvvxVeEqMkGGbCZKHX_vCgnIp0iE_OWyICjVTFPitQJ00fXLhyHyPb7q5J605tuK2iTHp2NCRJEXIAl9e0F_qASBBAfyL0C4FCBtvbnEMwtpoV1VWinkKgkI7JVH0AsyTugjXyAjxxsJxBTJT9qwZLxVBoaxgqNTOFfxvwstyq1VfCl3iBbpt71D" -} - - -def test_transaction_count_min_5min(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=5minutes&date_from=2023-11-01T12:00:00.000&date_to=2023-11-01T12:04:59.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [(3, 1, 2, 0)] - - -def test_transaction_count_min_30min(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=5minutes&date_from=2023-11-01T12:00:00.000&date_to=2023-11-01T12:29:59.999", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 6 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [ - (3, 1, 2, 0), - (2, 0, 0, 0), - (0, 0, 0, 0), - (1, 0, 1, 0), - (2, 1, 0, 0), - (3, 0, 0, 2), - ] - - -def test_transaction_count_min_1h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=5minutes&date_from=2023-11-01T12:00:00.000&date_to=2023-11-01T12:59:59.999", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 12 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [ - (3, 1, 2, 0), - (2, 0, 0, 0), - (0, 0, 0, 0), - (1, 0, 1, 0), - (2, 1, 0, 0), - (3, 0, 0, 2), - (0, 0, 0, 0), - (1, 0, 0, 0), - (2, 0, 0, 0), - (0, 0, 0, 0), - (5, 0, 2, 0), - (4, 1, 2, 0), - ] - - -def test_transaction_count_min_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=5minutes&date_from=2023-10-01T00:00.000&date_to=2023-10-31T00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_count_hour_30min(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=hour&date_from=2023-11-01T12:00:00&date_to=2023-11-01T12:30:00", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [(11, 2, 3, 2)] - - -def test_transaction_count_hour_1h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=hour&date_from=2023-11-01T12:00:00.000&date_to=2023-11-01T13:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 2 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [(23, 3, 7, 2), (0, 0, 0, 0)] - - -def test_transaction_count_hour_24h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=hour&date_from=2023-11-01T12:00:00.000&date_to=2023-11-02T11:59:59.999", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 24 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [ - (23, 3, 7, 2), - (1, 0, 0, 0), - (1, 0, 2, 0), - (1, 1, 0, 0), - (0, 0, 0, 0), - (1, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (2, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (2, 0, 0, 2), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (4, 0, 0, 0), - (0, 0, 0, 0), - (1, 0, 0, 0), - (1, 0, 0, 0), - ] - - -def test_transaction_count_hour_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=hour&date_from=2023-10-01T00:00.000&date_to=2023-10-31T00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_count_day_6h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=day&date_from=2023-11-01T12:00:00.000&date_to=2023-11-01T18:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [(27, 4, 9, 2)] - - -def test_transaction_count_day_1d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=day&date_from=2023-11-01T00:00.000&date_to=2023-11-01T00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [(29, 4, 9, 2)] - - -def test_transaction_count_day_24h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=day&date_from=2023-11-01T13:00:00.000&date_to=2023-11-02T13:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 2 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [(6, 1, 2, 0), (9, 0, 0, 2)] - - -def test_transaction_count_day_7d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=day&date_from=2023-11-01T12:00:00.000&date_to=2023-11-07T11:59:59.999", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 7 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [ - (29, 4, 9, 2), - (10, 0, 0, 2), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (1, 1, 1, 0), - (0, 0, 0, 0), - ] - - -def test_transaction_count_day_1mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=day&date_from=2023-11-01T12:00:00.000&date_to=2023-11-30T12:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 30 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [ - (29, 4, 9, 2), - (10, 0, 0, 2), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (1, 1, 1, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (3, 0, 0, 0), - (1, 0, 0, 0), - (0, 0, 2, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (1, 0, 0, 0), - (0, 0, 0, 0), - (1, 0, 0, 0), - (1, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (0, 0, 0, 0), - (1, 0, 0, 1), - (0, 0, 0, 0), - (0, 0, 0, 0), - (1, 0, 0, 0), - (0, 0, 0, 0), - (1, 0, 0, 0), - ] - - -def test_transaction_count_week_3d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=week&date_from=2023-11-01T12:00:00.000&date_to=2023-11-03T12:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [(39, 4, 9, 4)] - - -def test_transaction_count_week_7d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=week&date_from=2023-11-01T12:00:00.000&date_to=2023-11-07T12:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 2 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [(40, 5, 10, 4), (0, 0, 0, 0)] - - -def test_transaction_count_week_2mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=week&date_from=2023-11-01T12:00:00.000&date_to=2023-12-31T00:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 9 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [ - (40, 5, 10, 4), - (4, 0, 0, 0), - (3, 0, 2, 0), - (1, 0, 0, 1), - (5, 1, 0, 0), - (3, 0, 0, 0), - (2, 0, 0, 0), - (0, 3, 0, 0), - (0, 0, 0, 0), - ] - - -def test_transaction_count_week_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=week&date_from=2024-03-29T00:00:00.000&date_to=2024-04-30T00:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_count_month_7d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=month&date_from=2023-11-01T12:00:00&date_to=2023-11-07T12:00:00", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [(40, 5, 10, 4)] - - -def test_transaction_count_month_1mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=month&date_from=2023-11-01T12:00:00.000&date_to=2023-11-30T12:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [(50, 5, 12, 5)] - - -def test_transaction_count_month_6mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=month&date_from=2023-10-01T12:00:00.000&date_to=2024-03-31T12:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 6 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [ - (0, 0, 0, 0), - (50, 5, 12, 5), - (10, 4, 0, 0), - (6, 1, 1, 1), - (0, 0, 0, 0), - (7, 4, 4, 1), - ] - - -def test_transaction_count_month_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=month&date_from=2024-05-01T12:00:00.000&date_to=2024-06-01T12:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_count_year_2mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=year&date_from=2023-11-01T12:00:00.000&date_to=2023-12-31T12:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [(59, 9, 12, 5)] - - -def test_transaction_count_year_2y(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=year&date_from=2023-11-01T12:00:00.000&date_to=2024-03-31T12:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 2 - assert [ - (stat["status_200"], stat["status_300"], stat["status_400"], stat["status_500"]) - for stat in response.json() - ] == [(60, 9, 12, 5), (13, 5, 5, 2)] - - -def test_transaction_count_year_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - transactions = read_transactions_from_csv() - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_count?project_id=project-test&period=year&date_from=2024-03-31T12:00:00.000&date_to=2024-12-31T12:00:00.000", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 diff --git a/backend/tests/test_speed_statistics.py b/backend/tests/test_speed_statistics.py deleted file mode 100644 index 8c3276ca..00000000 --- a/backend/tests/test_speed_statistics.py +++ /dev/null @@ -1,972 +0,0 @@ -from utils import read_transactions_from_csv, truncate_float - -header = { - "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyMTAyODc3OTUzNDg0MzUyNDI3IiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiYm54OW9WT1o4U3FJOTcyczBHYjd4dyIsIm5hbWUiOiJUZXN0IFVzZXIiLCJwaWN0dXJlIjoiIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJpYXQiOjE3MTM3MzQ0NjEsImV4cCI6OTk5OTk5OTk5OX0.eZYMQzcSRzkAq4Me8C6SNU3wduS7EIu_o5XGAbsDmU05GtyipQEb5iNJ1QiLg-11RbZFL3dvi8xKd3mpuw8b-5l6u8hwSpZg6wNPLY0zPX-EOwxeHLtev_2X5pUf1_IWAnso9K_knsK8CcmJoVsCyNNjlw3hrkChacJHGNzg0TTT1rh3oe6KCpbLvYlV6tUPfm5k3AMFZIT7Jntr38CZvs6gac6L_DhItJc3TNNUUHie2zgA29_r9YFlaEr_nGoSmBhIi-i0i0h34TL4JAb4qJkVM2YI2eTTv2HjEGtkx4mE5JvNQ0VxzHSJcCNOHh1gCiFD5c6rhvvxVeEqMkGGbCZKHX_vCgnIp0iE_OWyICjVTFPitQJ00fXLhyHyPb7q5J605tuK2iTHp2NCRJEXIAl9e0F_qASBBAfyL0C4FCBtvbnEMwtpoV1VWinkKgkI7JVH0AsyTugjXyAjxxsJxBTJT9qwZLxVBoaxgqNTOFfxvwstyq1VfCl3iBbpt71D" -} - - -def test_transaction_speed_min_5min(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=5minutes&date_from=2023-11-01T12:00:00&date_to=2023-11-01T12:04:59", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [tuple([31.77272037])] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert temp == validation - - -def test_transaction_speed_min_30min(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=5minutes&date_from=2023-11-01T12:00:00&date_to=2023-11-01T12:29:59", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [ - tuple([0, 31.77272037, 0, 0]), - tuple([0, 2.94217308, 7.700000136, 16.00000129]), - tuple([0, 0, 0, 0]), - tuple([65.13026753, 0, 0, 0]), - tuple([0, 0, 82.41802468, 0]), - tuple([92.09210559, 89.41178679, 0, 20.00000035]), - ] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 6 - assert temp == validation - - -def test_transaction_speed_min_1h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=5minutes&date_from=2023-11-01T12:00:00&date_to=2023-11-01T12:59:59", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [ - tuple([0, 31.77272037, 0, 0]), - tuple([0, 2.94217308, 7.700000136, 16.00000129]), - tuple([0, 0, 0, 0]), - tuple([65.13026753, 0, 0, 0]), - tuple([0, 0, 82.41802468, 0]), - tuple([92.09210559, 89.41178679, 0, 20.00000035]), - tuple([0, 0, 0, 0]), - tuple([0, 98.46880746, 0, 0]), - tuple([0, 67.43749926, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 38.45918092, 0, 0]), - tuple([0, 0, 80.65413879, 55.3845944]), - ] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 12 - assert temp == validation - - -def test_transaction_speed_min_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=5minutes&date_from=2023-10-01T12:00:00&date_to=2023-10-31T12:30:00", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_speed_hour_30min(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=hour&date_from=2023-11-01T12:00:00&date_to=2023-11-01T12:30:00", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [tuple([78.61118656, 38.97485015, 57.5120165, 18.00000082])] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert temp == validation - - -def test_transaction_speed_hour_1h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=hour&date_from=2023-11-01T12:00:00&date_to=2023-11-01T13:00:00", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [ - tuple([78.61118656, 48.46159236, 69.08307764, 30.46153202]), - tuple([0, 0, 0, 0]), - ] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 2 - assert temp == validation - - -def test_transaction_speed_hour_24h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=hour&date_from=2023-11-01T12:00:00&date_to=2023-11-02T11:59:59", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [ - tuple([78.61118656, 48.46159236, 69.08307764, 30.46153202]), - tuple([0, 4.124278, 0, 0]), - tuple([0, 41.66667439, 0, 0]), - tuple([97.51860847, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([52.25000421, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([53.2270015, 23.00000908, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 5.988021267, 44.1666656]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([31.50684656, 0, 47.52925421, 0]), - tuple([0, 0, 0, 0]), - tuple([57.48084108, 0, 0, 0]), - tuple([0, 0, 32.00001264, 0]), - ] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 24 - assert temp == validation - - -def test_transaction_speed_hour_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=hour&date_from=2023-10-01T12:00:00&date_to=2023-10-31T18:00:00", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_speed_day_6h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=day&date_from=2023-11-01T12:00:00&date_to=2023-11-01T18:00:00", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [tuple([76.74774645, 44.80929007, 69.08307764, 30.46153202])] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert temp == validation - - -def test_transaction_speed_day_24h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=day&date_from=2023-11-01&date_to=2023-11-01", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [tuple([72.04359746, 43.355338, 69.08307764, 30.46153202])] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert temp == validation - - -def test_transaction_speed_day_1d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=day&date_from=2023-11-01T13:00:00&date_to=2023-11-02T13:00:00", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [ - tuple([67.66520472, 22.93032058, 0, 0]), - tuple([54.33745498, 0, 36.11515931, 44.1666656]), - ] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 2 - assert temp == validation - - -def test_transaction_speed_day_7d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=day&date_from=2023-11-01T12:00:00&date_to=2023-11-07T11:59:59", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [ - tuple([72.04359746, 43.355338, 69.08307764, 30.46153202]), - tuple([49.37809126, 0, 36.11515931, 44.1666656]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([23.46938859, 0, 0, 0]), - tuple([0, 0, 0, 0]), - ] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 7 - assert temp == validation - - -def test_transaction_speed_day_1mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=day&date_from=2023-11-01T12:00:00&date_to=2023-11-30T12:00:00", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [ - tuple([72.04359746, 43.355338, 69.08307764, 30.46153202]), - tuple([49.37809126, 0, 36.11515931, 44.1666656]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([23.46938859, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 73.48172621]), - tuple([0, 0, 86.68341626, 5.546371285]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 30.14705791, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 30.77519425, 0, 0]), - tuple([0, 79.79794645, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 14.50000117]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 83.11703415]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 73.73913033, 0]), - ] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 30 - assert temp == validation - - -def test_transaction_speed_day_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=day&date_from=2023-11-03T12:00:00&date_to=2023-11-04T12:00:00", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_speed_week_3d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=week&date_from=2023-11-01T12:00:00&date_to=2023-11-03T12:00:00", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [tuple([61.97003915, 43.355338, 54.09766022, 33.88781541])] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert temp == validation - - -def test_transaction_speed_week_7d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=week&date_from=2023-11-01T12:00:00&date_to=2023-11-07T12:00:00", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [ - tuple([58.11997409, 43.355338, 54.09766022, 33.88781541]), - tuple([0, 0, 0, 0]), - ] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 2 - assert temp == validation - - -def test_transaction_speed_week_2mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=week&date_from=2023-11-01T12:00:00&date_to=2023-12-31T00:00:00", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [ - tuple([58.11997409, 43.355338, 54.09766022, 33.88781541]), - tuple([0, 0, 86.68341626, 50.8366079]), - tuple([0, 55.28657035, 30.14705791, 0]), - tuple([0, 0, 0, 14.50000117]), - tuple([0, 0, 49.86956757, 57.39410621]), - tuple([54.36081291, 45.90951084, 0, 0]), - tuple([11.46285769, 72.06896586, 0, 0]), - tuple([0, 0, 0, 0]), - tuple([0, 0, 0, 0]), - ] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 9 - assert temp == validation - - -def test_transaction_speed_week_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=week&date_from=2024-03-29T00:00:00&date_to=2024-04-12T00:00:00", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_speed_month_7d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=month&date_from=2023-11-01T12:00:00&date_to=2023-11-07T12:00:00", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [tuple([58.119971, 43.355338, 54.09766022, 33.88781541])] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert temp == validation - - -def test_transaction_speed_month_1mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=month&date_from=2023-11-01&date_to=2023-11-30", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [tuple([58.11997409, 44.75901239, 56.11741906, 42.85312452])] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert temp == validation - - -def test_transaction_speed_month_6mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=month&date_from=2023-10-01&date_to=2024-03-31", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [ - tuple([0, 0, 0, 0]), - tuple([58.11997409, 44.75901239, 56.11741906, 42.85312452]), - tuple([32.9118353, 55.815765, 26.00000482, 44.53264224]), - tuple([38.85257592, 11.75, 17.26086978, 69.94543473]), - tuple([0, 0, 0, 0]), - tuple([55.78817778, 0, 0, 60.88811508]), - ] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 6 - assert temp == validation - - -def test_transaction_speed_month_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=month&date_from=2024-05-01&date_to=2024-06-01", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_speed_year_2mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=year&date_from=2023-11-01&date_to=2023-12-31", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [tuple([53.91861763, 44.82339021, 54.10959145, 43.15849138])] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert temp == validation - - -def test_transaction_speed_year_2y(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=year&date_from=2023-11-01&date_to=2024-03-31", - headers=header, - ) - temp = list( - [ - tuple( - [ - truncate_float(record["tokens_per_second"], 3) - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - validation = [ - tuple([53.91861763, 47.27191158, 54.10959145, 43.15849138]), - tuple([48.5300627, 11.75, 17.26086978, 63.15244499]), - ] - validation = list( - [tuple(map(lambda x: truncate_float(x, 3), list(val))) for val in validation] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 2 - assert temp == validation - - -def test_transaction_speed_year_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_speed?project_id=project-test&period=year&date_from=2024-05-31&date_to=2024-12-31", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 diff --git a/backend/tests/test_tokens_and_cost_statistics.py b/backend/tests/test_tokens_and_cost_statistics.py deleted file mode 100644 index 598259e1..00000000 --- a/backend/tests/test_tokens_and_cost_statistics.py +++ /dev/null @@ -1,1333 +0,0 @@ -from utils import read_transactions_from_csv - -header = { - "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyMTAyODc3OTUzNDg0MzUyNDI3IiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiYm54OW9WT1o4U3FJOTcyczBHYjd4dyIsIm5hbWUiOiJUZXN0IFVzZXIiLCJwaWN0dXJlIjoiIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJpYXQiOjE3MTM3MzQ0NjEsImV4cCI6OTk5OTk5OTk5OX0.eZYMQzcSRzkAq4Me8C6SNU3wduS7EIu_o5XGAbsDmU05GtyipQEb5iNJ1QiLg-11RbZFL3dvi8xKd3mpuw8b-5l6u8hwSpZg6wNPLY0zPX-EOwxeHLtev_2X5pUf1_IWAnso9K_knsK8CcmJoVsCyNNjlw3hrkChacJHGNzg0TTT1rh3oe6KCpbLvYlV6tUPfm5k3AMFZIT7Jntr38CZvs6gac6L_DhItJc3TNNUUHie2zgA29_r9YFlaEr_nGoSmBhIi-i0i0h34TL4JAb4qJkVM2YI2eTTv2HjEGtkx4mE5JvNQ0VxzHSJcCNOHh1gCiFD5c6rhvvxVeEqMkGGbCZKHX_vCgnIp0iE_OWyICjVTFPitQJ00fXLhyHyPb7q5J605tuK2iTHp2NCRJEXIAl9e0F_qASBBAfyL0C4FCBtvbnEMwtpoV1VWinkKgkI7JVH0AsyTugjXyAjxxsJxBTJT9qwZLxVBoaxgqNTOFfxvwstyq1VfCl3iBbpt71D" -} - - -def test_transaction_tokens_and_cost_min_5min(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=5minutes&date_from=2023-11-01T12:00:00&date_to=2023-11-01T12:04:59", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - costs = list( - [ - tuple([record["total_cost"] for record in date["records"]]) - for date in response.json() - ] - ) - tokens_validation = [tuple([174])] - costs_validation = [tuple([0.0003155])] - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_min_30min(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=5minutes&date_from=2023-11-01T12:00:00&date_to=2023-11-01T12:29:59", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [ - tuple([0, 174, 0, 0]), - tuple([0, 730, 142, 332]), - tuple([0, 730, 142, 332]), - tuple([107, 730, 142, 332]), - tuple([107, 730, 1816, 332]), - tuple([419, 835, 1816, 497]), - ] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [ - tuple([0, 0.0003155, 0, 0]), - tuple([0, 0.0013275, 0.0000568, 0.01092]), - tuple([0, 0.0013275, 0.0000568, 0.01092]), - tuple([0.001896, 0.0013275, 0.0000568, 0.01092]), - tuple([0.001896, 0.0013275, 0.0007264, 0.01092]), - tuple([0.008808, 0.001523, 0.0007264, 0.01887]), - ] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 6 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_min_1h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=5minutes&date_from=2023-11-01T12:00:00&date_to=2023-11-01T12:59:59", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [ - tuple([0, 174, 0, 0]), - tuple([0, 730, 142, 332]), - tuple([0, 730, 142, 332]), - tuple([107, 730, 142, 332]), - tuple([107, 730, 1816, 332]), - tuple([419, 835, 1816, 497]), - tuple([419, 835, 1816, 497]), - tuple([419, 2847, 1816, 497]), - tuple([419, 3713, 1816, 497]), - tuple([419, 3713, 1816, 497]), - tuple([419, 7904, 1816, 497]), - tuple([419, 7904, 2778, 578]), - ] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [ - tuple([0, 0.0003155, 0, 0]), - tuple([0, 0.0013275, 0.0000568, 0.01092]), - tuple([0, 0.0013275, 0.0000568, 0.01092]), - tuple([0.001896, 0.0013275, 0.0000568, 0.01092]), - tuple([0.001896, 0.0013275, 0.0007264, 0.01092]), - tuple([0.008808, 0.001523, 0.0007264, 0.01887]), - tuple([0.008808, 0.001523, 0.0007264, 0.01887]), - tuple([0.008808, 0.005541, 0.0007264, 0.01887]), - tuple([0.008808, 0.007212, 0.0007264, 0.01887]), - tuple([0.008808, 0.007212, 0.0007264, 0.01887]), - tuple([0.008808, 0.0151335, 0.0007264, 0.01887]), - tuple([0.008808, 0.0151335, 0.0011112, 0.02292]), - ] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 12 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_min_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=5minutes&date_from=2023-10-01T12:00:00&date_to=2023-10-31T12:30:00", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_tokens_and_cost_hour_30min(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=hour&date_from=2023-11-01T12:00:00&date_to=2023-11-01T12:30:00", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [tuple([419, 835, 1816, 497])] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [tuple([0.008808, 0.001523, 0.0007264, 0.01887])] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_hour_1h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=hour&date_from=2023-11-01T12:00:00&date_to=2023-11-01T13:00:00", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [tuple([419, 7904, 2778, 578]), tuple([419, 7904, 2778, 578])] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [ - tuple([0.008808, 0.0151335, 0.0011112, 0.02292]), - tuple([0.008808, 0.0151335, 0.0011112, 0.02292]), - ] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 2 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_hour_24h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=hour&date_from=2023-11-01T12:00:00&date_to=2023-11-02T11:59:59", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [ - tuple([419, 7904, 2778, 578]), - tuple([419, 7953, 2778, 578]), - tuple([419, 8045, 2778, 578]), - tuple([1231, 8045, 2778, 578]), - tuple([1231, 8045, 2778, 578]), - tuple([1468, 8045, 2778, 578]), - tuple([1468, 8045, 2778, 578]), - tuple([1468, 8045, 2778, 578]), - tuple([1781, 8079, 2778, 578]), - tuple([1781, 8079, 2778, 578]), - tuple([1781, 8079, 2778, 578]), - tuple([1781, 8079, 2778, 578]), - tuple([1781, 8079, 2778, 578]), - tuple([1781, 8079, 2778, 578]), - tuple([1781, 8079, 2778, 578]), - tuple([1781, 8079, 2778, 578]), - tuple([1781, 8079, 2787, 941]), - tuple([1781, 8079, 2787, 941]), - tuple([1781, 8079, 2787, 941]), - tuple([1781, 8079, 2787, 941]), - tuple([1871, 8079, 4429, 941]), - tuple([1871, 8079, 4429, 941]), - tuple([2253, 8079, 4429, 941]), - tuple([2253, 8079, 5337, 941]), - ] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [ - tuple([0.008808, 0.0151335, 0.0011112, 0.02292]), - tuple([0.008808, 0.0152295, 0.0011112, 0.02292]), - tuple([0.008808, 0.01538, 0.0011112, 0.02292]), - tuple([0.02788, 0.01538, 0.0011112, 0.02292]), - tuple([0.02788, 0.01538, 0.0011112, 0.02292]), - tuple([0.03312, 0.01538, 0.0011112, 0.02292]), - tuple([0.03312, 0.01538, 0.0011112, 0.02292]), - tuple([0.03312, 0.01538, 0.0011112, 0.02292]), - tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), - tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), - tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), - tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), - tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), - tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), - tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), - tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), - tuple([0.040216, 0.0154425, 0.0011148, 0.04176]), - tuple([0.040216, 0.0154425, 0.0011148, 0.04176]), - tuple([0.040216, 0.0154425, 0.0011148, 0.04176]), - tuple([0.040216, 0.0154425, 0.0011148, 0.04176]), - tuple([0.041304, 0.0154425, 0.0017716, 0.04176]), - tuple([0.041304, 0.0154425, 0.0017716, 0.04176]), - tuple([0.04988, 0.0154425, 0.0017716, 0.04176]), - tuple([0.04988, 0.0154425, 0.0021348, 0.04176]), - ] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 24 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_hour_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=hour&date_from=2023-10-01T12:00:00&date_to=2023-10-31T18:00:00", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_tokens_and_cost_day_6h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=day&date_from=2023-11-01T12:00:00&date_to=2023-11-01T18:00:00", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [tuple([1468, 8045, 2778, 578])] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [tuple([0.03312, 0.01538, 0.0011112, 0.02292])] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_day_24h(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=day&date_from=2023-11-01&date_to=2023-11-01", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [tuple([1781, 8079, 2778, 578])] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [tuple([0.040216, 0.0154425, 0.0011112, 0.02292])] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_day_1d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=day&date_from=2023-11-01T13:00:00&date_to=2023-11-02T13:00:00", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [tuple([1362, 175, 0, 0]), tuple([2085, 175, 2559, 363])] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [ - tuple([0.031408, 0.000309, 0.0, 0.0]), - tuple([0.046632, 0.000309, 0.0010236, 0.01884]), - ] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 2 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_day_7d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=day&date_from=2023-11-01T12:00:00&date_to=2023-11-07T11:59:59", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [ - tuple([1781, 8079, 2778, 578]), - tuple([2805, 8079, 5337, 941]), - tuple([2805, 8079, 5337, 941]), - tuple([2805, 8079, 5337, 941]), - tuple([2805, 8079, 5337, 941]), - tuple([2902, 8079, 5337, 941]), - tuple([2902, 8079, 5337, 941]), - ] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [ - tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), - tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), - tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), - tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), - tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), - tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), - tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), - ] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 7 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_day_1mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=day&date_from=2023-11-01T12:00:00&date_to=2023-11-30T12:00:00", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [ - tuple([1781, 8079, 2778, 578]), - tuple([2805, 8079, 5337, 941]), - tuple([2805, 8079, 5337, 941]), - tuple([2805, 8079, 5337, 941]), - tuple([2805, 8079, 5337, 941]), - tuple([2902, 8079, 5337, 941]), - tuple([2902, 8079, 5337, 941]), - tuple([2902, 8079, 5337, 941]), - tuple([2902, 8079, 5337, 941]), - tuple([2902, 8079, 5337, 941]), - tuple([2902, 8079, 5337, 941]), - tuple([2902, 8079, 5337, 1390]), - tuple([2902, 8079, 6348, 2090]), - tuple([2902, 8079, 6348, 2090]), - tuple([2902, 8079, 6348, 2090]), - tuple([2902, 8079, 6348, 2090]), - tuple([2902, 8079, 6631, 2090]), - tuple([2902, 8079, 6631, 2090]), - tuple([2902, 8498, 6631, 2090]), - tuple([2902, 8643, 6631, 2090]), - tuple([2902, 8643, 6631, 2090]), - tuple([2902, 8643, 6631, 2090]), - tuple([2902, 8643, 6631, 2090]), - tuple([2902, 8643, 6631, 2090]), - tuple([2902, 8643, 6631, 2152]), - tuple([2902, 8643, 6631, 2152]), - tuple([2902, 8643, 6631, 2152]), - tuple([2902, 8643, 6631, 32357]), - tuple([2902, 8643, 6631, 32357]), - tuple([2902, 8643, 13616, 32357]), - ] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [ - tuple([0.040216, 0.0154425, 0.0011112, 0.02292]), - tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), - tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), - tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), - tuple([0.062264, 0.0154425, 0.0021348, 0.04176]), - tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), - tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), - tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), - tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), - tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), - tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), - tuple([0.063408, 0.0154425, 0.0021348, 0.06414]), - tuple([0.063408, 0.0154425, 0.0025392, 0.10527]), - tuple([0.063408, 0.0154425, 0.0025392, 0.10527]), - tuple([0.063408, 0.0154425, 0.0025392, 0.10527]), - tuple([0.063408, 0.0154425, 0.0025392, 0.10527]), - tuple([0.063408, 0.0154425, 0.0026524, 0.10527]), - tuple([0.063408, 0.0154425, 0.0026524, 0.10527]), - tuple([0.063408, 0.0162695, 0.0026524, 0.10527]), - tuple([0.063408, 0.0165265, 0.0026524, 0.10527]), - tuple([0.063408, 0.0165265, 0.0026524, 0.10527]), - tuple([0.063408, 0.0165265, 0.0026524, 0.10527]), - tuple([0.063408, 0.0165265, 0.0026524, 0.10527]), - tuple([0.063408, 0.0165265, 0.0026524, 0.10527]), - tuple([0.063408, 0.0165265, 0.0026524, 0.108]), - tuple([0.063408, 0.0165265, 0.0026524, 0.108]), - tuple([0.063408, 0.0165265, 0.0026524, 0.108]), - tuple([0.063408, 0.0165265, 0.0026524, 1.9143]), - tuple([0.063408, 0.0165265, 0.0026524, 1.9143]), - tuple([0.063408, 0.0165265, 0.0054464, 1.9143]), - ] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 30 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_day_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=day&date_from=2023-11-03T12:00:00&date_to=2023-11-04T12:00:00", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_tokens_and_cost_week_3d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=week&date_from=2023-11-01T12:00:00&date_to=2023-11-03T12:00:00", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [tuple([2805, 8079, 5337, 941])] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [tuple([0.062264, 0.0154425, 0.0021348, 0.04176])] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_week_7d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=week&date_from=2023-11-01T12:00:00&date_to=2023-11-07T12:00:00", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [tuple([2902, 8079, 5337, 941]), tuple([2902, 8079, 5337, 941])] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [ - tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), - tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), - ] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 2 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_week_2mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=week&date_from=2023-11-01T12:00:00&date_to=2023-12-31T00:00:00", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [ - tuple([2902, 8079, 5337, 941]), - tuple([2902, 8079, 6348, 2090]), - tuple([2902, 8643, 6631, 2090]), - tuple([2902, 8643, 6631, 2152]), - tuple([2902, 8643, 13730, 33279]), - tuple([3877, 8859, 13730, 33279]), - tuple([4682, 9159, 13730, 33279]), - tuple([4682, 9159, 13730, 33279]), - tuple([4682, 9159, 13730, 33279]), - ] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [ - tuple([0.063408, 0.0154425, 0.0021348, 0.04176]), - tuple([0.063408, 0.0154425, 0.0025392, 0.10527]), - tuple([0.063408, 0.0165265, 0.0026524, 0.10527]), - tuple([0.063408, 0.0165265, 0.0026524, 0.108]), - tuple([0.063408, 0.0165265, 0.005492, 1.96632]), - tuple([0.085768, 0.016896, 0.005492, 1.96632]), - tuple([0.104496, 0.0174505, 0.005492, 1.96632]), - tuple([0.104496, 0.0174505, 0.005492, 1.96632]), - tuple([0.104496, 0.0174505, 0.005492, 1.96632]), - ] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 9 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_week_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=week&date_from=2024-03-29T00:00:00&date_to=2024-04-12T00:00:00", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_tokens_and_cost_month_7d(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=month&date_from=2023-11-01T12:00:00&date_to=2023-11-07T12:00:00", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [tuple([2902, 8079, 5337, 941])] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [tuple([0.063408, 0.0154425, 0.0021348, 0.04176])] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_month_1mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=month&date_from=2023-11-01&date_to=2023-11-30", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [tuple([2902, 8643, 13616, 32357])] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [tuple([0.063408, 0.0165265, 0.0054464, 1.9143])] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_month_6mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=month&date_from=2023-10-01&date_to=2024-03-31", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [ - tuple([0, 0, 0, 0]), - tuple([2902, 8643, 13616, 32357]), - tuple([4682, 9384, 13730, 33279]), - tuple([4975, 9804, 14278, 34344]), - tuple([4975, 9804, 14278, 34344]), - tuple([5586, 9804, 14278, 61424]), - ] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [ - tuple([0.0, 0.0, 0.0, 0.0]), - tuple([0.063408, 0.0165265, 0.0054464, 1.9143]), - tuple([0.104496, 0.0178535, 0.005492, 1.96632]), - tuple([0.11116, 0.0186715, 0.0057112, 2.02788]), - tuple([0.11116, 0.0186715, 0.0057112, 2.02788]), - tuple([0.123792, 0.0186715, 0.0057112, 3.5808]), - ] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 6 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_month_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=month&date_from=2024-05-01&date_to=2024-06-01", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 - - -def test_transaction_tokens_and_cost_year_2mo(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=year&date_from=2023-11-01&date_to=2023-12-31", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [tuple([4682, 9245, 13730, 33279])] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [tuple([0.104496, 0.017596, 0.005492, 1.96632])] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 1 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_year_2y(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=year&date_from=2023-11-01&date_to=2024-03-31", - headers=header, - ) - tokens = list( - [ - tuple( - [ - record["input_cumulative_total"] + record["output_cumulative_total"] - for record in date["records"] - ] - ) - for date in response.json() - ] - ) - tokens_validation = [ - tuple([4682, 9384, 13730, 33279]), - tuple([5586, 9804, 14278, 61424]), - ] - - costs = list( - [ - tuple( - [round(record["total_cost"] * 1000000, 1) for record in date["records"]] - ) - for date in response.json() - ] - ) - costs_validation = [ - tuple([0.104496, 0.0178535, 0.005492, 1.96632]), - tuple([0.123792, 0.0186715, 0.0057112, 3.5808]), - ] - costs_validation = list( - [ - tuple(map(lambda x: round(x * 1000000, 1), list(val))) - for val in costs_validation - ] - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 2 - assert tokens == tokens_validation - assert costs == costs_validation - - -def test_transaction_tokens_and_cost_year_empty(client, application): - # arrange - with application.transaction_context() as ctx: - repo = ctx["transaction_repository"] - repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv( - "../test_transactions_tokens_cost_speed.csv" - ) - for transaction in transactions: - repo.add(transaction) - - # act - response = client.get( - "/api/statistics/transactions_cost?project_id=project-test&period=year&date_from=2024-05-31&date_to=2024-12-31", - headers=header, - ) - - # assert - assert response.status_code == 200 - assert len(response.json()) == 0 diff --git a/backend/tests/test_transactions_pricing.py b/backend/tests/test_transactions_pricing.py index e7c78a07..a5112797 100644 --- a/backend/tests/test_transactions_pricing.py +++ b/backend/tests/test_transactions_pricing.py @@ -1,4 +1,4 @@ -from utils import read_transactions_from_csv +from test_utils import read_transactions_with_prices_from_csv header = { "Authorization": "Bearer eyJhbGciOiJSUzI1NiIsImN0eSI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0IiwiYXpwIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwiYXVkIjoiNDA3NDA4NzE4MTkyLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29tIiwic3ViIjoiMTEyMTAyODc3OTUzNDg0MzUyNDI3IiwiZW1haWwiOiJ0ZXN0QGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhdF9oYXNoIjoiYm54OW9WT1o4U3FJOTcyczBHYjd4dyIsIm5hbWUiOiJUZXN0IFVzZXIiLCJwaWN0dXJlIjoiIiwiZ2l2ZW5fbmFtZSI6IlRlc3QiLCJmYW1pbHlfbmFtZSI6IlVzZXIiLCJpYXQiOjE3MTM3MzQ0NjEsImV4cCI6OTk5OTk5OTk5OX0.eZYMQzcSRzkAq4Me8C6SNU3wduS7EIu_o5XGAbsDmU05GtyipQEb5iNJ1QiLg-11RbZFL3dvi8xKd3mpuw8b-5l6u8hwSpZg6wNPLY0zPX-EOwxeHLtev_2X5pUf1_IWAnso9K_knsK8CcmJoVsCyNNjlw3hrkChacJHGNzg0TTT1rh3oe6KCpbLvYlV6tUPfm5k3AMFZIT7Jntr38CZvs6gac6L_DhItJc3TNNUUHie2zgA29_r9YFlaEr_nGoSmBhIi-i0i0h34TL4JAb4qJkVM2YI2eTTv2HjEGtkx4mE5JvNQ0VxzHSJcCNOHh1gCiFD5c6rhvvxVeEqMkGGbCZKHX_vCgnIp0iE_OWyICjVTFPitQJ00fXLhyHyPb7q5J605tuK2iTHp2NCRJEXIAl9e0F_qASBBAfyL0C4FCBtvbnEMwtpoV1VWinkKgkI7JVH0AsyTugjXyAjxxsJxBTJT9qwZLxVBoaxgqNTOFfxvwstyq1VfCl3iBbpt71D" @@ -10,7 +10,13 @@ def test_openai_costs(client, application): with application.transaction_context() as ctx: repo = ctx["transaction_repository"] repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv("../test_transactions_pricing.csv") + + config = application['config'] + + transactions = read_transactions_with_prices_from_csv( + "test_transactions_pricing.csv", + config.PRICE_LIST_PATH + ) for transaction in transactions: repo.add(transaction) @@ -84,7 +90,13 @@ def test_azure_costs(client, application): with application.transaction_context() as ctx: repo = ctx["transaction_repository"] repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv("../test_transactions_pricing.csv") + + config = application['config'] + + transactions = read_transactions_with_prices_from_csv( + "test_transactions_pricing.csv", + config.PRICE_LIST_PATH + ) for transaction in transactions: repo.add(transaction) @@ -186,7 +198,13 @@ def test_anthropic_costs(client, application): with application.transaction_context() as ctx: repo = ctx["transaction_repository"] repo.delete_cascade(project_id="project-test") - transactions = read_transactions_from_csv("../test_transactions_pricing.csv") + + config = application['config'] + + transactions = read_transactions_with_prices_from_csv( + "test_transactions_pricing.csv", + config.PRICE_LIST_PATH + ) for transaction in transactions: repo.add(transaction) diff --git a/backend/tests/test_utils.py b/backend/tests/test_utils.py index f2ccb015..97e46432 100644 --- a/backend/tests/test_utils.py +++ b/backend/tests/test_utils.py @@ -1,4 +1,10 @@ -from utils import detect_subdomain +from utils import detect_subdomain, read_provider_pricelist + +from datetime import datetime, timedelta +import pandas as pd +import re +from transactions.models import Transaction +from pathlib import Path def test_detect_subdomain(): @@ -29,3 +35,207 @@ def test_local_domains(): host = "project1.promptsail.local:8000" base_url = "http://promptsail.local:8000" assert detect_subdomain(host, base_url) == "project1" + + +def truncate_float(number, decimals): + if isinstance(number, float): + str_number = str(number) + integer_part, decimal_part = str_number.split(".") + truncated_decimal_part = decimal_part[:decimals] + return float(f"{integer_part}.{truncated_decimal_part}") + return number + + +def read_transactions_from_csv(fixture_file_name: str) -> list[Transaction]: + """Read test transactions from a CSV file. + + The CSV files are located in the fixtures/transactions directory. + + The CSV file should have the following columns: + provider;model;total_tokens;input_tokens;output_tokens;status_code;request_time;response_time + + Example: + OpenAI;babbage-002;216;200;16;200;2024-04-18T11:11:34.098Z;2024-04-18T11:11:34.490Z + OpenAI;davinci-002;62;46;16;200;2024-04-18T11:24:05.948Z;2024-04-18T11:24:06.573Z + + Args: + fixture_file_name: Name of the fixture file in the fixtures/transactions directory. + + Returns: + List of Transaction objects + """ + # Get path to fixtures directory relative to this file + fixtures_dir = Path(__file__).parent / "fixtures" + path = fixtures_dir / "transactions" / fixture_file_name + + # check if file exists + if not path.exists(): + raise FileNotFoundError(f"File {path} does not exist. Check if the file exists in the fixtures/transactions directory.") + + df = pd.read_csv(path, sep=";") + data = df.to_dict(orient="records") + transactions = [] + + for idx, obj in enumerate(data): + transaction_id = f"test-transaction-{idx}" + request_time = datetime.fromisoformat(obj["request_time"].replace("Z", "+00:00")) + response_time = datetime.fromisoformat(obj["response_time"].replace("Z", "+00:00")) + latency = response_time - request_time + + # Set default values + base_transaction = { + "id": transaction_id, + "project_id": "project-test", + "request": {}, + "response": {}, + "tags": [ f"tag-{idx%2}"], + "provider": obj["provider"], + "model": obj["model"], + "type": "chat", + "os": None, + "library": "PostmanRuntime/7.36.3", + "status_code": obj["status_code"], + "messages": None, + "prompt": "", + "last_message": "", + "request_time": request_time, + "response_time": response_time, + } + + # Add status-specific values + if obj["status_code"] == 200: + base_transaction.update({ + "input_tokens": obj["input_tokens"], + "output_tokens": obj["output_tokens"], + "error_message": None, + "generation_speed": obj["output_tokens"] / latency.total_seconds() if latency.total_seconds() > 0 else 0, + "input_cost": 0, + "output_cost": 0, + "total_cost": 0, + }) + else: + base_transaction.update({ + "input_tokens": 0, + "output_tokens": 0, + "error_message": "Error", + "generation_speed": 0, + "input_cost": 0, + "output_cost": 0, + "total_cost": 0, + }) + + transactions.append(Transaction(**base_transaction)) + + return transactions + + +def read_transactions_with_prices_from_csv(fixture_file_name: str, providers_pricelist_path: str) -> list[Transaction]: + """Read the important fields of test transactions with prices from a CSV file and create a list of Transaction objects. + The rest of the fields like tags, os, library are set by the function. + + The CSV file should be located in the fixtures/transactions directory and have the following columns: + provider;model;total_tokens;input_tokens;output_tokens;status_code;request_time;response_time + + """ + # Get path to fixtures directory relative to this file + fixtures_dir = Path(__file__).parent / "fixtures" + path = fixtures_dir / "transactions" / fixture_file_name + + # check if file exists + if not path.exists(): + raise FileNotFoundError(f"File {path} does not exist. Check if the file exists in the fixtures/transactions directory.") + + + df = pd.read_csv(path, sep=";") + data = df.to_dict(orient="records") + transactions = [] + pricelist = read_provider_pricelist(providers_pricelist_path) + for idx, obj in enumerate(data): + transaction_id = f"test-transaction-{idx}" + request_time = datetime.fromisoformat( + obj["request_time"].replace("Z", "+00:00") + ) + response_time = datetime.fromisoformat( + obj["response_time"].replace("Z", "+00:00") + ) + latency = response_time - request_time + price = [ + item + for item in pricelist + if item.provider == obj["provider"] + and re.match(item.match_pattern, obj["model"]) + ] + if obj["status_code"] == 200: + if len(price) > 0: + price = price[0] + if price.input_price == 0: + input_cost, output_cost = 0, 0 + total_cost = ( + (obj["input_tokens"] + obj["output_tokens"]) + / 1000 + * price.total_price + ) + else: + input_cost = price.input_price * (obj["input_tokens"] / 1000) + output_cost = price.output_price * (obj["output_tokens"] / 1000) + total_cost = input_cost + output_cost + else: + input_cost, output_cost, total_cost = None, None, None + transactions.append( + Transaction( + id=transaction_id, + project_id="project-test", + request={}, + response={}, + tags=[ f"tag-{idx%2}"], + provider=obj["provider"], + model=obj["model"], + type="chat", + os=None, + input_tokens=obj["input_tokens"], + output_tokens=obj["output_tokens"], + library="PostmanRuntime/7.36.3", + status_code=obj["status_code"], + messages=None, + prompt="", + last_message="", + error_message=None, + request_time=request_time, + response_time=response_time, + generation_speed=obj["output_tokens"] / latency.total_seconds() + if latency.total_seconds() > 0 + else 0, + input_cost=input_cost, + output_cost=output_cost, + total_cost=total_cost, + ) + ) + else: + transactions.append( + Transaction( + id=transaction_id, + project_id="project-test", + request={}, + response={}, + tags=[ f"tag-{idx%2}"], + provider=obj["provider"], + model=obj["model"], + type="chat", + os=None, + input_tokens=0, + output_tokens=0, + library="PostmanRuntime/7.36.3", + status_code=obj["status_code"], + messages=None, + prompt="", + last_message="", + error_message="Error", + request_time=request_time, + response_time=response_time, + generation_speed=0, + input_cost=0, + output_cost=0, + total_cost=0, + ) + ) + return transactions \ No newline at end of file diff --git a/coding_prompts/refactor_test.md b/coding_prompts/refactor_test.md new file mode 100644 index 00000000..78bd29c9 --- /dev/null +++ b/coding_prompts/refactor_test.md @@ -0,0 +1,44 @@ + + + +Please recommend the way how to organize and structure the tests that checks counting transations and costs logic, how to properly group, setup all necessary preliminary data, how to clean the database in @test_file follow the best practises form @Pytest and @FastAPI documentation. + + + +1. Proper test function names. + +Check if all the test functions has proper names with the same naming pattern, propose the new names if needed: + - test___ + - examples: + test_5min_duration_with_5min_granularity_returns_single_interval_with_tokens_and_costs + test_30min_duration_with_5min_granularity_returns_six_intervals_with_tokens_and_costs + +2. Proper test function docstrings. +Check and propose the new docstrings if needed for all the test functions with the following structure,do not change the test name and test logic, focus only on docstring. Docstring format: + - One line description that describes what the test case is about. Test name and description should be in sync. + - Given: Some initial data that is needed to run the test. + - When: Some action that is needed to run the test. + - Then: Expected result of the test. + Example: + ''' + Tests token usage and cost statistics for a 5-minute time frame with 5-minute granularity. + + Given: Transactions spanning 5 minutes (2023-11-01 12:00-12:05) + When: Requesting token and cost statistics with 5-minute granularity + Then: Returns one interval with correct token counts and costs for each model + ''' +3. Review and design proper test classes. +Review the test functions body and test names and try to group them into logical test cases, each test case should be a class, create new file for new test cases with suffix _refactored.py. Examples of Test Classes: + - tests for counting transactions: TestCountTransactionsStatistics + - tests for counting costs: TestCountCostsStatistics + - tests for checking costs logic: TestCheckCostsLogic + - tests for checking transactions logic: TestCheckTransactionsLogic + +4. Perform refactoring create base class with common logic. +Create the base class for all the test cases, extract all the common logic to the base class, setup all necessary data, clean the database after each test. +- create base class with setup and teardown methods +- extract making request logic to the separate method +- extract logic to check response status and length to the separate method + +5. Compare original and refactored test files. +Review the new test cases, check if you do not miss any test method, compere original and refactored test files. diff --git a/docs/_posts/2025-06-15-PromptSail-has-stoped.md b/docs/_posts/2025-06-15-PromptSail-has-stoped.md new file mode 100644 index 00000000..1a4c406e --- /dev/null +++ b/docs/_posts/2025-06-15-PromptSail-has-stoped.md @@ -0,0 +1,40 @@ +--- +title: "Prompt Sail - No longer maintain" +last_modified_at: 2025-06-15T10:00:58 +tags: + - "" + - "prompt-sail" + - "llm-api-proxy" +category: + - Project Updates +toc: false +toc_label: "Unique Title" +toc_icon: "heart" +--- + +## Prompt Sail is no longer maintain + + +### Charting a New Course: Sunsetting PromptSail and Unveiling a PyTorch Adventure! + +Ahoy, sailors and navigators of the digital sea! + +First and foremost, a heartfelt thank you to everyone who has been part of the PromptSail journey. Your support, feedback, and enthusiasm have been the wind in our sails, and we're incredibly grateful for the community that formed around our project. + +However, all voyages must eventually reach a port. We've made the difficult decision to drop anchor on PromptSail, and the project will no longer be actively maintained as we shift our focus to a new, exciting endeavor. + +### A New Horizon: The Ultimate PyTorch Course + +While one chapter closes, another—crackling with energy and possibility—opens. We are thrilled to announce our new project: a comprehensive, free online **[PyTorch course](https://pytorchcourse.com)**! + +This isn't just another tutorial. We are diving deep into the core building blocks of modern AI, from the fundamentals of tensors to the advanced architectures that power today's Transformers and Diffusion models. + +### Meet Professor Torchenstein! + +Guiding you on this new adventure is the brilliant, if slightly eccentric, **Professor Victor py Torchenstein**. With a spark of genius in his eyes and a laugh that echoes in the halls of his lab, the Professor believes that mastering PyTorch is the key to computational glory. He's ready to turn you from a mere student into a true PyTorch master, or as he calls them, his "acolytes." + +He was last heard muttering, *"They called me mad! And they were right! Madly efficient at PyTorch!"* + +So, while we say a fond farewell to PromptSail, we invite you to join us and Professor Torchenstein on a new, thrilling quest. It's time to build empires... of code! + +Set your course for **[pytorchcourse.com](https://pytorchcourse.com)** and let the learning begin! diff --git a/ui/Dockerfile b/ui/Dockerfile index e80fb70a..05a6b822 100644 --- a/ui/Dockerfile +++ b/ui/Dockerfile @@ -1,12 +1,12 @@ # ---- Base Node ---- -FROM node:20-alpine AS base +FROM node:20.18.0-alpine AS dependencies LABEL org.opencontainers.image.source="https://github.com/PromptSail/prompt_sail" WORKDIR /app COPY package*.json ./ # ---- Dependencies ---- -FROM base AS dependencies -RUN apk --no-cache add curl +#FROM base AS dependencies +#RUN apk --no-cache add curl RUN npm ci && npm cache clean --force # ---- Copy Files/Build ---- @@ -21,7 +21,7 @@ RUN npm run build --omit=dev # Stage 2, use the compiled app, ready for production with Nginx -FROM nginx:1.21.6-alpine +FROM nginx:1.25-alpine-slim COPY --from=build /app/dist /usr/share/nginx/html @@ -33,4 +33,19 @@ COPY /deployment/docker_env_replacement.sh /docker-entrypoint.d/docker_env_repla RUN chmod +x /docker-entrypoint.d/docker_env_replacement.sh +# Set proper permissions and switch to non-root user +RUN chown -R nginx:nginx /usr/share/nginx/html && \ + chmod -R 755 /usr/share/nginx/html && \ + chown -R nginx:nginx /var/cache/nginx && \ + chown -R nginx:nginx /var/log/nginx && \ + chown -R nginx:nginx /etc/nginx/conf.d +RUN touch /var/run/nginx.pid && \ + chown -R nginx:nginx /var/run/nginx.pid +USER nginx + +EXPOSE 80 + +HEALTHCHECK --interval=30s --timeout=3s CMD wget --quiet --tries=1 --spider http://localhost:80 || exit 1 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/ui/package-lock.json b/ui/package-lock.json index 0e3035f5..7b1fd252 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -1,19 +1,19 @@ { - "name": "prompt-sail-web", - "version": "0.0.0", + "name": "promptsail-ui", + "version": "0.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "prompt-sail-web", - "version": "0.0.0", + "name": "promptsail-ui", + "version": "0.5.4", "dependencies": { "@ant-design/compatible": "^5.1.2", "@ant-design/icons": "^5.3.1", "@azure/msal-browser": "^3.13.0", "@react-oauth/google": "^0.12.1", "antd": "^5.17.3", - "axios": "^1.6.2", + "axios": "^1.7.4", "d3-scale-chromatic": "^3.1.0", "dotenv": "^16.3.1", "formik": "^2.4.5", @@ -29,7 +29,7 @@ "react-svg": "^16.1.32", "react-syntax-highlighter": "^15.5.0", "recharts": "^2.13.0-alpha.4", - "sass": "^1.69.5", + "sass": "^1.80.3", "slugify": "^1.6.6", "trendline": "^1.0.0", "yup": "^1.3.2" @@ -50,7 +50,7 @@ "postcss": "^8.4.31", "tailwindcss": "^3.3.5", "typescript": "^5.2.2", - "vite": "^5.0.0" + "vite": "^5.4.6" } }, "node_modules/@alloc/quick-lru": { @@ -58,6 +58,7 @@ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -70,6 +71,7 @@ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -79,9 +81,10 @@ } }, "node_modules/@ant-design/colors": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.0.2.tgz", - "integrity": "sha512-7KJkhTiPiLHSu+LmMJnehfJ6242OCxSlR3xHVBecYxnMW8MS/878NXct1GqYARyL59fyeFdKRxXTfvR9SnDgJg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.1.0.tgz", + "integrity": "sha512-MMoDGWn1y9LdQJQSHiCC20x3uZ3CwQnv9QMz6pCmJOrqdgM9YxsoVVY0wtrdXbmfSgnV0KNk6zi09NAhMR2jvg==", + "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^3.6.1" } @@ -90,6 +93,7 @@ "version": "5.1.3", "resolved": "https://registry.npmjs.org/@ant-design/compatible/-/compatible-5.1.3.tgz", "integrity": "sha512-uLvjwENnpY/1gxYb5m8L2k+WmBpBHqH45fO76+4J+WUDKTUKpmwbEW3OG/in2mSO2HI6h3Mcp57+tAMr18ZkgQ==", + "license": "MIT", "dependencies": { "classnames": "^2.2.6", "dayjs": "^1.11.4", @@ -108,12 +112,14 @@ "node_modules/@ant-design/css-animation": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@ant-design/css-animation/-/css-animation-1.7.3.tgz", - "integrity": "sha512-LrX0OGZtW+W6iLnTAqnTaoIsRelYeuLZWsrmBJFUXDALQphPsN8cE5DCsmoSlL0QYb94BQxINiuS70Ar/8BNgA==" + "integrity": "sha512-LrX0OGZtW+W6iLnTAqnTaoIsRelYeuLZWsrmBJFUXDALQphPsN8cE5DCsmoSlL0QYb94BQxINiuS70Ar/8BNgA==", + "license": "MIT" }, "node_modules/@ant-design/cssinjs": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.20.0.tgz", - "integrity": "sha512-uG3iWzJxgNkADdZmc6W0Ci3iQAUOvLMcM8SnnmWq3r6JeocACft4ChnY/YWvI2Y+rG/68QBla/O+udke1yH3vg==", + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-1.21.1.tgz", + "integrity": "sha512-tyWnlK+XH7Bumd0byfbCiZNK43HEubMoCcu9VxwsAwiHdHTgWa+tMN0/yvxa+e8EzuFP1WdUNNPclRpVtD33lg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", @@ -121,21 +127,49 @@ "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", - "stylis": "^4.0.13" + "stylis": "^4.3.3" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.1.tgz", + "integrity": "sha512-2HAiyGGGnM0es40SxdszeQAU5iWp41wBIInq+ONTCKjlSKOrzQfnw4JDtB8IBmqE6tQaEKwmzTP2LGdt5DSwYQ==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^1.21.0", + "@babel/runtime": "^7.23.2", + "rc-util": "^5.38.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-2.0.6.tgz", + "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + } + }, "node_modules/@ant-design/icons": { - "version": "5.3.7", - "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.3.7.tgz", - "integrity": "sha512-bCPXTAg66f5bdccM4TT21SQBDO1Ek2gho9h3nO9DAKXJP4sq+5VBjrQMSxMVXSB3HyEz+cUbHQ5+6ogxCOpaew==", + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-5.5.1.tgz", + "integrity": "sha512-0UrM02MA2iDIgvLatWrj6YTCYe0F/cwXvVE0E2SqGrL7PZireQwgEKTKBisWpZyal5eXZLvuM98kju6YtYne8w==", + "license": "MIT", "dependencies": { "@ant-design/colors": "^7.0.0", "@ant-design/icons-svg": "^4.4.0", - "@babel/runtime": "^7.11.2", + "@babel/runtime": "^7.24.8", "classnames": "^2.2.6", "rc-util": "^5.31.1" }, @@ -150,12 +184,14 @@ "node_modules/@ant-design/icons-svg": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", - "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==" + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==", + "license": "MIT" }, "node_modules/@ant-design/react-slick": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-1.1.2.tgz", "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.4", "classnames": "^2.2.5", @@ -168,31 +204,34 @@ } }, "node_modules/@azure/msal-browser": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.15.0.tgz", - "integrity": "sha512-jqngIR0zGLtEHCAhgXLl+VZTFcU/9DmRSjGj5RbrLnFPL/0L9Hr68k8grvLrTIq7tjhTM5Xgh6Xc0l7JlViHQQ==", + "version": "3.26.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.26.1.tgz", + "integrity": "sha512-y78sr9g61aCAH9fcLO1um+oHFXc1/5Ap88RIsUSuzkm0BHzFnN+PXGaQeuM1h5Qf5dTnWNOd6JqkskkMPAhh7Q==", + "license": "MIT", "dependencies": { - "@azure/msal-common": "14.10.0" + "@azure/msal-common": "14.15.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "14.10.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.10.0.tgz", - "integrity": "sha512-Zk6DPDz7e1wPgLoLgAp0349Yay9RvcjPM5We/ehuenDNsz/t9QEFI7tRoHpp/e47I4p20XE3FiDlhKwAo3utDA==", + "version": "14.15.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.15.0.tgz", + "integrity": "sha512-ImAQHxmpMneJ/4S8BRFhjt1MZ3bppmpRPYYNyzeQPeFN288YKbb8TmmISQEbtfkQ1BPASvYZU5doIZOPBAqENQ==", + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@babel/code-frame": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", - "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.6", + "@babel/highlight": "^7.25.7", "picocolors": "^1.0.0" }, "engines": { @@ -200,30 +239,32 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz", - "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.6.tgz", - "integrity": "sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.8.tgz", + "integrity": "sha512-Oixnb+DzmRT30qu9d3tJSQkxuygWm32DFykT4bRoORPa9hZ/L4KhVB/XiRm6KG+roIEM7DBQlmg27kw2HZkdZg==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.6", - "@babel/generator": "^7.24.6", - "@babel/helper-compilation-targets": "^7.24.6", - "@babel/helper-module-transforms": "^7.24.6", - "@babel/helpers": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/template": "^7.24.6", - "@babel/traverse": "^7.24.6", - "@babel/types": "^7.24.6", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.8", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.8", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -243,34 +284,37 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz", - "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.6", + "@babel/types": "^7.25.7", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz", - "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.24.6", - "@babel/helper-validator-option": "^7.24.6", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -283,67 +327,36 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", - "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", - "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", - "dev": true, - "dependencies": { - "@babel/template": "^7.24.6", - "@babel/types": "^7.24.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz", - "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz", - "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.6" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz", - "integrity": "sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-environment-visitor": "^7.24.6", - "@babel/helper-module-imports": "^7.24.6", - "@babel/helper-simple-access": "^7.24.6", - "@babel/helper-split-export-declaration": "^7.24.6", - "@babel/helper-validator-identifier": "^7.24.6" + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -353,85 +366,81 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz", - "integrity": "sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz", - "integrity": "sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==", - "dev": true, - "dependencies": { - "@babel/types": "^7.24.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", - "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.6" + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz", - "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", - "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz", - "integrity": "sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz", - "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.24.6", - "@babel/types": "^7.24.6" + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", - "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.6", + "@babel/helper-validator-identifier": "^7.25.7", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" @@ -441,10 +450,14 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", - "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.8" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -453,12 +466,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.6.tgz", - "integrity": "sha512-FfZfHXtQ5jYPQsCRyLpOv2GeLIIJhs8aydpNh39vRDjhD411XcfWDni5i7OjP/Rs8GAtTn7sWFFELJSHqkIxYg==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.7.tgz", + "integrity": "sha512-JD9MUnLbPL0WdVK8AWC7F7tTG2OS6u/AKKnsK+NdRhUiVdnzyR1S3kKQCaRLOiaULvUiqK6Z4JQE635VgtCFeg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.6" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -468,12 +482,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.6.tgz", - "integrity": "sha512-BQTBCXmFRreU3oTUXcGKuPOfXAGb1liNY4AvvFKsOBAJ89RKcTsIrSsnMYkj59fNa66OFKnSa4AJZfy5Y4B9WA==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.7.tgz", + "integrity": "sha512-S/JXG/KrbIY06iyJPKfxr0qRxnhNOdkNXYBl/rmwgDd72cQLH9tEGkDm/yJPGvcSIUoikzfjMios9i+xT/uv9w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.24.6" + "@babel/helper-plugin-utils": "^7.25.7" }, "engines": { "node": ">=6.9.0" @@ -483,9 +498,10 @@ } }, "node_modules/@babel/runtime": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz", - "integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.7.tgz", + "integrity": "sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==", + "license": "MIT", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -494,33 +510,32 @@ } }, "node_modules/@babel/template": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", - "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6" + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz", - "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.24.6", - "@babel/generator": "^7.24.6", - "@babel/helper-environment-visitor": "^7.24.6", - "@babel/helper-function-name": "^7.24.6", - "@babel/helper-hoist-variables": "^7.24.6", - "@babel/helper-split-export-declaration": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6", + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -529,13 +544,14 @@ } }, "node_modules/@babel/types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", - "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.6", - "@babel/helper-validator-identifier": "^7.24.6", + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -546,6 +562,7 @@ "version": "3.6.1", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", "engines": { "node": ">=10" } @@ -553,21 +570,24 @@ "node_modules/@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" }, "node_modules/@emotion/unitless": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -577,13 +597,14 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -593,13 +614,14 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -609,13 +631,14 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -625,13 +648,14 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -641,13 +665,14 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -657,13 +682,14 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -673,13 +699,14 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -689,13 +716,14 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -705,13 +733,14 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -721,13 +750,14 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -737,13 +767,14 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -753,13 +784,14 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -769,13 +801,14 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -785,13 +818,14 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -801,13 +835,14 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -817,13 +852,14 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -833,13 +869,14 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -849,13 +886,14 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -865,13 +903,14 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -881,13 +920,14 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -897,13 +937,14 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -913,13 +954,14 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -933,6 +975,7 @@ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -944,10 +987,11 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -957,6 +1001,7 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -980,6 +1025,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -990,6 +1036,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -1005,6 +1052,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1017,6 +1065,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -1025,21 +1074,24 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -1052,6 +1104,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1062,6 +1115,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1074,6 +1128,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -1086,13 +1141,16 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -1106,10 +1164,11 @@ } }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1122,6 +1181,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1137,6 +1197,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -1151,6 +1212,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -1160,21 +1222,24 @@ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1185,6 +1250,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1198,6 +1264,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -1207,6 +1274,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1215,11 +1283,285 @@ "node": ">= 8" } }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" @@ -1229,6 +1571,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz", "integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.24.4" }, @@ -1237,12 +1580,13 @@ } }, "node_modules/@rc-component/color-picker": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-1.5.3.tgz", - "integrity": "sha512-+tGGH3nLmYXTalVe0L8hSZNs73VTP5ueSHwUlDC77KKRaN7G4DS4wcpG5DTDzdcV/Yas+rzA6UGgIyzd8fS4cw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-2.0.1.tgz", + "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==", + "license": "MIT", "dependencies": { + "@ant-design/fast-color": "^2.0.6", "@babel/runtime": "^7.23.6", - "@ctrl/tinycolor": "^3.6.1", "classnames": "^2.2.6", "rc-util": "^5.38.1" }, @@ -1255,6 +1599,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-1.4.0.tgz", "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "rc-util": "^5.27.0" @@ -1268,6 +1613,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz", "integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.0" }, @@ -1279,6 +1625,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", @@ -1296,6 +1643,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-1.1.2.tgz", "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", @@ -1309,10 +1657,29 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@rc-component/qrcode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.0.0.tgz", + "integrity": "sha512-L+rZ4HXP2sJ1gHMGHjsg9jlYBX/SLN2D6OxP9Zn3qgtpMWtO2vUfxVFwiogHpAIqs54FnALxraUy/BCO1yRIgg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.7", + "classnames": "^2.3.2", + "rc-util": "^5.38.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, "node_modules/@rc-component/tour": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.0.tgz", - "integrity": "sha512-h6hyILDwL+In9GAgRobwRWihLqqsD7Uft3fZGrJ7L4EiyCoxbnNYwzPXDfz7vNDhWeVyvAWQJj9fJCzpI4+b4g==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-1.15.1.tgz", + "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.0", "@rc-component/portal": "^1.0.0-9", @@ -1329,9 +1696,10 @@ } }, "node_modules/@rc-component/trigger": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.0.tgz", - "integrity": "sha512-QarBCji02YE9aRFhZgRZmOpXBj0IZutRippsVBv85sxvG4FGk/vRxwAlkn3MS9zK5mwbETd86mAVg2tKqTkdJA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-2.2.3.tgz", + "integrity": "sha512-X1oFIpKoXAMXNDYCviOmTfuNuYxE4h5laBsyCqVAVMjNHxoF3/uiyA7XdegK1XbCvBbCZ6P6byWrEoDRpKL8+A==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", @@ -1352,222 +1720,240 @@ "version": "0.12.1", "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.1.tgz", "integrity": "sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==", + "license": "MIT", "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "node_modules/@remix-run/router": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz", - "integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz", + "integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==", + "license": "MIT", "engines": { "node": ">=14.0.0" } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1577,6 +1963,7 @@ "version": "10.1.68", "resolved": "https://registry.npmjs.org/@tanem/svg-injector/-/svg-injector-10.1.68.tgz", "integrity": "sha512-UkJajeR44u73ujtr5GVSbIlELDWD/mzjqWe54YMK61ljKxFcJoPd9RBSaO7xj02ISCWUqJW99GjrS+sVF0UnrA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.2", "content-type": "^1.0.5", @@ -1588,6 +1975,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -1601,6 +1989,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" } @@ -1610,6 +1999,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -1620,6 +2010,7 @@ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" } @@ -1627,22 +2018,26 @@ "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", "dependencies": { "@types/d3-color": "*" } @@ -1650,12 +2045,14 @@ "node_modules/@types/d3-path": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "license": "MIT" }, "node_modules/@types/d3-scale": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "license": "MIT", "dependencies": { "@types/d3-time": "*" } @@ -1664,12 +2061,14 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-shape": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "license": "MIT", "dependencies": { "@types/d3-path": "*" } @@ -1677,23 +2076,27 @@ "node_modules/@types/d3-time": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", - "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/hast": { "version": "2.3.10", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", "dependencies": { "@types/unist": "^2" } @@ -1702,6 +2105,7 @@ "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "license": "MIT", "dependencies": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" @@ -1711,36 +2115,41 @@ "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { - "version": "20.12.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz", - "integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==", + "version": "20.16.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.13.tgz", + "integrity": "sha512-GjQ7im10B0labo8ZGXDGROUl9k0BNyDgzfGpb4g/cl+4yYDWVKcozANF4FGr4/p0O/rAkQClM6Wiwkije++1Tg==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/prop-types": { - "version": "15.7.12", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "version": "18.3.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", + "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", + "license": "MIT", "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/react": "*" } @@ -1750,6 +2159,7 @@ "resolved": "https://registry.npmjs.org/@types/react-syntax-highlighter/-/react-syntax-highlighter-15.5.13.tgz", "integrity": "sha512-uLGJ87j6Sz8UaBAooU0T6lWJ0dBmjZgN1PZTrj05TNql2/XpC6+4HhMT5syIdFUUt+FASfCeLLv4kBygNU+8qA==", "dev": true, + "license": "MIT", "dependencies": { "@types/react": "*" } @@ -1758,18 +2168,21 @@ "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/unist": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.5.1", "@typescript-eslint/scope-manager": "6.21.0", @@ -1805,6 +2218,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "6.21.0", "@typescript-eslint/types": "6.21.0", @@ -1833,6 +2247,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0" @@ -1850,6 +2265,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "6.21.0", "@typescript-eslint/utils": "6.21.0", @@ -1877,6 +2293,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, + "license": "MIT", "engines": { "node": "^16.0.0 || >=18.0.0" }, @@ -1890,6 +2307,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "6.21.0", "@typescript-eslint/visitor-keys": "6.21.0", @@ -1918,6 +2336,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", @@ -1943,6 +2362,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/types": "6.21.0", "eslint-visitor-keys": "^3.4.1" @@ -1959,17 +2379,19 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.0.tgz", - "integrity": "sha512-KcEbMsn4Dpk+LIbHMj7gDPRKaTMStxxWRkRmxsg/jVdFdJCZWt1SchZcf0M4t8lIKdwwMsEyzhrcOXRrDPtOBw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.3.tgz", + "integrity": "sha512-NooDe9GpHGqNns1i8XDERg0Vsg5SSYRhRxxyTGogUdkdNt47jal+fbuYi+Yfq6pzRCKXyoPcWisfxE6RIM3GKA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/core": "^7.24.5", - "@babel/plugin-transform-react-jsx-self": "^7.24.5", - "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@babel/core": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", "@types/babel__core": "^7.20.5", "react-refresh": "^0.14.2" }, @@ -1981,10 +2403,11 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1997,6 +2420,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2005,6 +2429,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/add-dom-event-listener/-/add-dom-event-listener-1.1.0.tgz", "integrity": "sha512-WCxx1ixHT0GQU9hb0KI/mhgRQhnU+U3GvwY6ZvVjYq8rsihIGoaIOUbY0yMPBxLH5MDtr0kz3fisWGNcbWW7Jw==", + "license": "MIT", "dependencies": { "object-assign": "4.x" } @@ -2014,6 +2439,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2030,6 +2456,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2039,6 +2466,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -2047,58 +2475,60 @@ } }, "node_modules/antd": { - "version": "5.17.4", - "resolved": "https://registry.npmjs.org/antd/-/antd-5.17.4.tgz", - "integrity": "sha512-oDWrcibe1s72223vpvA3/dNBEotGkggyWQVX1+GVrhuVXt/QYE3oU3Tsg3PeMurohvO8kjxambqG/zbmsMG34g==", - "dependencies": { - "@ant-design/colors": "^7.0.2", - "@ant-design/cssinjs": "^1.19.1", - "@ant-design/icons": "^5.3.7", + "version": "5.21.4", + "resolved": "https://registry.npmjs.org/antd/-/antd-5.21.4.tgz", + "integrity": "sha512-yMpwam1A4/RIIemJK0V3SpMAfgbBEM47OFzEYcEQPDP+B4ZAeviKOLaFFxUt/sxRCMeoALnJEK6Hb6qOqL0hbA==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^7.1.0", + "@ant-design/cssinjs": "^1.21.1", + "@ant-design/cssinjs-utils": "^1.1.1", + "@ant-design/icons": "^5.5.1", "@ant-design/react-slick": "~1.1.2", - "@babel/runtime": "^7.24.5", + "@babel/runtime": "^7.25.6", "@ctrl/tinycolor": "^3.6.1", - "@rc-component/color-picker": "~1.5.3", + "@rc-component/color-picker": "~2.0.1", "@rc-component/mutate-observer": "^1.1.0", - "@rc-component/tour": "~1.15.0", - "@rc-component/trigger": "^2.1.1", + "@rc-component/qrcode": "~1.0.0", + "@rc-component/tour": "~1.15.1", + "@rc-component/trigger": "^2.2.3", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", - "dayjs": "^1.11.10", - "qrcode.react": "^3.1.0", - "rc-cascader": "~3.26.0", + "dayjs": "^1.11.11", + "rc-cascader": "~3.28.1", "rc-checkbox": "~3.3.0", - "rc-collapse": "~3.7.3", - "rc-dialog": "~9.4.0", - "rc-drawer": "~7.1.0", + "rc-collapse": "~3.8.0", + "rc-dialog": "~9.6.0", + "rc-drawer": "~7.2.0", "rc-dropdown": "~4.2.0", - "rc-field-form": "~2.0.1", - "rc-image": "~7.6.0", - "rc-input": "~1.5.1", - "rc-input-number": "~9.1.0", - "rc-mentions": "~2.13.1", - "rc-menu": "~9.14.0", - "rc-motion": "^2.9.1", - "rc-notification": "~5.4.0", - "rc-pagination": "~4.0.4", - "rc-picker": "~4.5.0", + "rc-field-form": "~2.4.0", + "rc-image": "~7.11.0", + "rc-input": "~1.6.3", + "rc-input-number": "~9.2.0", + "rc-mentions": "~2.16.1", + "rc-menu": "~9.15.1", + "rc-motion": "^2.9.3", + "rc-notification": "~5.6.2", + "rc-pagination": "~4.3.0", + "rc-picker": "~4.6.15", "rc-progress": "~4.0.0", - "rc-rate": "~2.12.0", + "rc-rate": "~2.13.0", "rc-resize-observer": "^1.4.0", - "rc-segmented": "~2.3.0", - "rc-select": "~14.14.0", - "rc-slider": "~10.6.2", + "rc-segmented": "~2.5.0", + "rc-select": "~14.15.2", + "rc-slider": "~11.1.7", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", - "rc-table": "~7.45.6", - "rc-tabs": "~15.1.0", - "rc-textarea": "~1.7.0", - "rc-tooltip": "~6.2.0", - "rc-tree": "~5.8.7", - "rc-tree-select": "~5.21.0", - "rc-upload": "~4.5.2", - "rc-util": "^5.41.0", + "rc-table": "~7.47.5", + "rc-tabs": "~15.3.0", + "rc-textarea": "~1.8.2", + "rc-tooltip": "~6.2.1", + "rc-tree": "~5.9.0", + "rc-tree-select": "~5.23.0", + "rc-upload": "~4.8.1", + "rc-util": "^5.43.0", "scroll-into-view-if-needed": "^3.1.0", - "throttle-debounce": "^5.0.0" + "throttle-debounce": "^5.0.2" }, "funding": { "type": "opencollective", @@ -2113,12 +2543,15 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -2131,24 +2564,28 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/array-tree-filter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-tree-filter/-/array-tree-filter-2.1.0.tgz", - "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==" + "integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==", + "license": "MIT" }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2161,12 +2598,13 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -2182,12 +2620,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -2201,9 +2640,10 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2214,6 +2654,7 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "license": "MIT", "dependencies": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -2222,17 +2663,20 @@ "node_modules/babel-runtime/node_modules/regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "license": "MIT" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/big-integer": { "version": "1.6.52", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", "engines": { "node": ">=0.6" } @@ -2241,6 +2685,8 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -2253,6 +2699,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -2261,6 +2708,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -2272,6 +2720,7 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/broadcast-channel/-/broadcast-channel-3.7.0.tgz", "integrity": "sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.7.2", "detect-node": "^2.1.0", @@ -2284,9 +2733,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", "dev": true, "funding": [ { @@ -2302,11 +2751,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -2320,6 +2770,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2329,14 +2780,15 @@ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001625", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz", - "integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==", + "version": "1.0.30001669", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001669.tgz", + "integrity": "sha512-DlWzFDJqstqtIVx1zeSpIMLjunf5SmwOw0N2Ck/QSQdS8PLS4+9HrLaYei4w8BIAL7IB/UEDu889d8vhCTPA0w==", "dev": true, "funding": [ { @@ -2351,13 +2803,15 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -2371,6 +2825,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -2380,6 +2835,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -2389,54 +2845,38 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 14.16.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" } }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -2446,6 +2886,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "1.1.3" } @@ -2454,12 +2895,14 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2471,6 +2914,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -2481,6 +2925,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -2488,17 +2933,20 @@ "node_modules/compute-scroll-into-view": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", - "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==", + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" }, "node_modules/content-type": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2507,12 +2955,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/copy-anything": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "license": "MIT", "dependencies": { "is-what": "^3.14.1" }, @@ -2524,6 +2974,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", "dependencies": { "toggle-selection": "^1.0.6" } @@ -2533,12 +2984,14 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true + "hasInstallScript": true, + "license": "MIT" }, "node_modules/create-react-class": { "version": "15.7.0", "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz", "integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==", + "license": "MIT", "dependencies": { "loose-envify": "^1.3.1", "object-assign": "^4.1.1" @@ -2549,6 +3002,7 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2563,6 +3017,7 @@ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, + "license": "MIT", "bin": { "cssesc": "bin/cssesc" }, @@ -2573,12 +3028,14 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", "dependencies": { "internmap": "1 - 2" }, @@ -2590,6 +3047,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -2598,6 +3056,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", "engines": { "node": ">=12" } @@ -2606,6 +3065,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -2614,6 +3074,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", "dependencies": { "d3-color": "1 - 3" }, @@ -2625,6 +3086,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", "engines": { "node": ">=12" } @@ -2633,6 +3095,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", @@ -2648,6 +3111,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" @@ -2660,6 +3124,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { "d3-path": "^3.1.0" }, @@ -2671,6 +3136,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", "dependencies": { "d3-array": "2 - 3" }, @@ -2682,6 +3148,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", "dependencies": { "d3-time": "1 - 3" }, @@ -2693,22 +3160,25 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/dayjs": { - "version": "1.11.11", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", - "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==" + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2722,18 +3192,21 @@ "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", - "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/deepmerge": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2742,26 +3215,42 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==" + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "license": "MIT" }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -2773,13 +3262,15 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -2791,6 +3282,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -2799,12 +3291,14 @@ "node_modules/dom-scroll-into-view": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-1.2.1.tgz", - "integrity": "sha512-LwNVg3GJOprWDO+QhLL1Z9MMgWe/KAFLxVWKzjRTxNSPn8/LLDIfmuG71YHznXCqaqTjvHJDYO1MEAgX6XCNbQ==" + "integrity": "sha512-LwNVg3GJOprWDO+QhLL1Z9MMgWe/KAFLxVWKzjRTxNSPn8/LLDIfmuG71YHznXCqaqTjvHJDYO1MEAgX6XCNbQ==", + "license": "MIT" }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -2816,24 +3310,28 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.4.787", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.787.tgz", - "integrity": "sha512-d0EFmtLPjctczO3LogReyM2pbBiiZbnsKnGF+cdZhsYzHm/A0GV7W94kqzLD8SN4O3f3iHlgLUChqghgyznvCQ==", - "dev": true + "version": "1.5.41", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.41.tgz", + "integrity": "sha512-dfdv/2xNjX0P8Vzme4cfzHqnPm5xsZXwsolTYr0eyW18IUmNyG08vL+fttvinTfhKfIKdRoqkDIC9e9iWQCNYQ==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "license": "MIT", "optional": true, "dependencies": { "prr": "~1.0.1" @@ -2843,11 +3341,12 @@ } }, "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -2855,36 +3354,37 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2894,21 +3394,24 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -2958,6 +3461,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2966,10 +3470,11 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", - "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.12.tgz", + "integrity": "sha512-9neVjoGv20FwYtCP6CB1dzR1vr57ZDNOXst21wd2xJ/cTlM2xLq0GWVlSNTdMn/4BtP6cHYBMCSp1wFBJ9jBsg==", "dev": true, + "license": "MIT", "peerDependencies": { "eslint": ">=7" } @@ -2979,6 +3484,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2995,6 +3501,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -3007,6 +3514,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -3022,6 +3530,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3032,6 +3541,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3048,6 +3558,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -3059,13 +3570,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3078,6 +3591,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -3093,6 +3607,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3102,6 +3617,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3114,6 +3630,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3126,6 +3643,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -3138,6 +3656,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -3151,10 +3670,11 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -3167,6 +3687,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -3179,6 +3700,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -3188,6 +3710,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -3195,18 +3718,21 @@ "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-equals": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz", "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -3216,6 +3742,7 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -3232,6 +3759,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -3243,19 +3771,22 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -3264,6 +3795,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", "dependencies": { "format": "^0.2.0" }, @@ -3277,6 +3809,7 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -3288,6 +3821,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3300,6 +3834,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3316,6 +3851,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -3329,18 +3865,20 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -3351,10 +3889,11 @@ } }, "node_modules/foreground-child": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", "dev": true, + "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -3367,9 +3906,10 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3397,6 +3937,7 @@ "url": "https://opencollective.com/formik" } ], + "license": "Apache-2.0", "dependencies": { "@types/hoist-non-react-statics": "^3.3.1", "deepmerge": "^2.1.1", @@ -3416,6 +3957,7 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", "dev": true, + "license": "MIT", "engines": { "node": "*" }, @@ -3427,12 +3969,14 @@ "node_modules/fs": { "version": "0.0.1-security", "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", - "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==", + "license": "ISC" }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -3445,13 +3989,16 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3465,6 +4012,7 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3474,6 +4022,7 @@ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -3483,6 +4032,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3503,6 +4053,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -3514,6 +4065,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3523,6 +4075,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3535,6 +4088,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -3544,6 +4098,7 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -3562,19 +4117,22 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -3584,6 +4142,7 @@ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -3595,6 +4154,7 @@ "version": "2.2.5", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -3604,6 +4164,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", @@ -3620,14 +4181,22 @@ "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", "engines": { "node": "*" } }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" } @@ -3636,6 +4205,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -3645,10 +4215,11 @@ } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -3657,6 +4228,7 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "license": "MIT", "optional": true, "bin": { "image-size": "bin/image-size.js" @@ -3666,15 +4238,17 @@ } }, "node_modules/immutable": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==" + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "license": "MIT" }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3691,6 +4265,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -3700,6 +4275,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -3708,12 +4284,14 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", "engines": { "node": ">=12" } @@ -3722,6 +4300,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3731,6 +4310,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" @@ -3744,6 +4324,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -3752,12 +4334,16 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3767,6 +4353,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3776,6 +4363,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3785,6 +4373,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3793,6 +4382,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -3804,6 +4394,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3813,6 +4404,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -3822,6 +4414,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3829,25 +4422,25 @@ "node_modules/is-what": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", - "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==" + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/jackspeak": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", - "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -3856,10 +4449,11 @@ } }, "node_modules/jiti": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", "dev": true, + "license": "MIT", "bin": { "jiti": "bin/jiti.js" } @@ -3867,18 +4461,21 @@ "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3887,39 +4484,44 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json2mq": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "license": "MIT", "dependencies": { "string-convert": "^0.2.0" } @@ -3929,6 +4531,7 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -3940,6 +4543,7 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -3951,6 +4555,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", "engines": { "node": ">=18" } @@ -3960,6 +4565,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -3968,6 +4574,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "license": "Apache-2.0", "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -3994,6 +4601,7 @@ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -4007,6 +4615,7 @@ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -4015,13 +4624,15 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -4035,33 +4646,39 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.upperfirst": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", - "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==" + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -4073,6 +4690,7 @@ "version": "1.20.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" @@ -4087,6 +4705,7 @@ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } @@ -4095,6 +4714,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", "optional": true, "dependencies": { "pify": "^4.0.1", @@ -4108,6 +4728,7 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", "optional": true, "bin": { "semver": "bin/semver" @@ -4117,6 +4738,7 @@ "version": "6.3.4", "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.4.tgz", "integrity": "sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.8", "remove-accents": "0.5.0" @@ -4127,15 +4749,16 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "dev": true, + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -4147,12 +4770,14 @@ "node_modules/microseconds": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/microseconds/-/microseconds-0.2.0.tgz", - "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==" + "integrity": "sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==", + "license": "MIT" }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", "optional": true, "bin": { "mime": "cli.js" @@ -4165,6 +4790,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4173,6 +4799,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -4185,6 +4812,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -4200,21 +4828,24 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -4225,6 +4856,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/nano-time/-/nano-time-1.0.0.tgz", "integrity": "sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==", + "license": "ISC", "dependencies": { "big-integer": "^1.6.16" } @@ -4240,6 +4872,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -4251,12 +4884,14 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/needle": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "license": "MIT", "optional": true, "dependencies": { "iconv-lite": "^0.6.3", @@ -4269,16 +4904,25 @@ "node": ">= 4.4.x" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4288,6 +4932,7 @@ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4296,6 +4941,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4305,6 +4951,7 @@ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } @@ -4312,12 +4959,14 @@ "node_modules/oblivious-set": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/oblivious-set/-/oblivious-set-1.0.0.tgz", - "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==" + "integrity": "sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==", + "license": "MIT" }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -4327,6 +4976,7 @@ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -4344,6 +4994,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -4359,6 +5010,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -4369,11 +5021,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -4385,6 +5045,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", @@ -4402,6 +5063,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -4410,6 +5072,7 @@ "version": "0.12.7", "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "license": "MIT", "dependencies": { "process": "^0.11.1", "util": "^0.10.3" @@ -4420,6 +5083,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4428,6 +5092,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -4437,6 +5102,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4445,13 +5111,15 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -4464,19 +5132,18 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", - "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "license": "ISC" }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4484,18 +5151,21 @@ "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -4507,6 +5177,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", "optional": true, "engines": { "node": ">=6" @@ -4517,14 +5188,15 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -4540,10 +5212,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -4554,6 +5227,7 @@ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", "dev": true, + "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -4571,6 +5245,7 @@ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", "dev": true, + "license": "MIT", "dependencies": { "camelcase-css": "^2.0.1" }, @@ -4600,6 +5275,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" @@ -4621,10 +5297,11 @@ } }, "node_modules/postcss-load-config/node_modules/lilconfig": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", "dev": true, + "license": "MIT", "engines": { "node": ">=14" }, @@ -4633,29 +5310,37 @@ } }, "node_modules/postcss-nested": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { - "postcss-selector-parser": "^6.0.11" + "postcss-selector-parser": "^6.1.1" }, "engines": { "node": ">=12.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, "peerDependencies": { "postcss": "^8.2.14" } }, "node_modules/postcss-selector-parser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", - "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, + "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -4668,13 +5353,15 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -4683,6 +5370,7 @@ "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "license": "MIT", "engines": { "node": ">=6" } @@ -4691,6 +5379,7 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", "engines": { "node": ">= 0.6.0" } @@ -4699,6 +5388,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -4708,12 +5398,14 @@ "node_modules/property-expr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", - "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==" + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT" }, "node_modules/property-information": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", "dependencies": { "xtend": "^4.0.0" }, @@ -4725,12 +5417,14 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "license": "MIT", "optional": true }, "node_modules/punycode": { @@ -4738,18 +5432,11 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/qrcode.react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz", - "integrity": "sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4768,12 +5455,14 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/raf": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", "dependencies": { "performance-now": "^2.1.0" } @@ -4782,6 +5471,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/rc-animate/-/rc-animate-3.1.1.tgz", "integrity": "sha512-8wg2Zg3EETy0k/9kYuis30NJNQg1D6/WSQwnCiz6SvyxQXNet/rVraRz3bPngwY6rcU2nlRvoShiYOorXyF7Sg==", + "license": "MIT", "dependencies": { "@ant-design/css-animation": "^1.7.2", "classnames": "^2.2.6", @@ -4793,6 +5483,7 @@ "version": "4.21.1", "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.21.1.tgz", "integrity": "sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==", + "license": "MIT", "dependencies": { "add-dom-event-listener": "^1.1.0", "prop-types": "^15.5.10", @@ -4802,15 +5493,16 @@ } }, "node_modules/rc-cascader": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.26.0.tgz", - "integrity": "sha512-L1dml383TPSJD1I11YwxuVbmqaJY64psZqFp1ETlgl3LEOwDu76Cyl11fw5dmjJhMlUWwM5dECQfqJgfebhUjg==", + "version": "3.28.2", + "resolved": "https://registry.npmjs.org/rc-cascader/-/rc-cascader-3.28.2.tgz", + "integrity": "sha512-8f+JgM83iLTvjgdkgU7GfI4qY8icXOBP0cGZjOdx2iJAkEe8ucobxDQAVE69UD/c3ehCxZlcgEHeD5hFmypbUw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", "array-tree-filter": "^2.1.0", "classnames": "^2.3.1", - "rc-select": "~14.14.0", - "rc-tree": "~5.8.1", + "rc-select": "~14.15.0", + "rc-tree": "~5.9.0", "rc-util": "^5.37.0" }, "peerDependencies": { @@ -4822,6 +5514,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/rc-checkbox/-/rc-checkbox-3.3.0.tgz", "integrity": "sha512-Ih3ZaAcoAiFKJjifzwsGiT/f/quIkxJoklW4yKGho14Olulwn8gN7hOBve0/WGDg5o/l/5mL0w7ff7/YGvefVw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", @@ -4833,9 +5526,10 @@ } }, "node_modules/rc-collapse": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.7.3.tgz", - "integrity": "sha512-60FJcdTRn0X5sELF18TANwtVi7FtModq649H11mYF1jh83DniMoM4MqY627sEKRCTm4+WXfGDcB7hY5oW6xhyw==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/rc-collapse/-/rc-collapse-3.8.0.tgz", + "integrity": "sha512-YVBkssrKPBG09TGfcWWGj8zJBYD9G3XuTy89t5iUmSXrIXEAnO1M+qjUxRW6b4Qi0+wNWG6MHJF/+US+nmIlzA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -4848,9 +5542,10 @@ } }, "node_modules/rc-dialog": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.4.0.tgz", - "integrity": "sha512-AScCexaLACvf8KZRqCPz12BJ8olszXOS4lKlkMyzDQHS1m0zj1KZMYgmMCh39ee0Dcv8kyrj8mTqxuLyhH+QuQ==", + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/rc-dialog/-/rc-dialog-9.6.0.tgz", + "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", @@ -4864,9 +5559,10 @@ } }, "node_modules/rc-drawer": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.1.0.tgz", - "integrity": "sha512-nBE1rF5iZvpavoyqhSSz2mk/yANltA7g3aF0U45xkx381n3we/RKs9cJfNKp9mSWCedOKWt9FLEwZDaAaOGn2w==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/rc-drawer/-/rc-drawer-7.2.0.tgz", + "integrity": "sha512-9lOQ7kBekEJRdEpScHvtmEtXnAsy+NGDXiRWc2ZVC7QXAazNVbeT4EraQKYwCME8BJLa8Bxqxvs5swwyOepRwg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.23.9", "@rc-component/portal": "^1.1.1", @@ -4883,6 +5579,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-4.2.0.tgz", "integrity": "sha512-odM8Ove+gSh0zU27DUj5cG1gNKg7mLWBYzB5E4nNLrLwBmYEgYP43vHKDGOVZcJSVElQBI0+jTQgjnq0NfLjng==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", "@rc-component/trigger": "^2.0.0", @@ -4895,9 +5592,10 @@ } }, "node_modules/rc-field-form": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.0.1.tgz", - "integrity": "sha512-3WK/POHBcfMFKrzScrkmgMIXqoVQ0KgVwcVnej/ukwuQG4ZHCJaTi2KhM+tWTK4WODBXbmjKg5pKHj2IVmSg4A==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/rc-field-form/-/rc-field-form-2.4.0.tgz", + "integrity": "sha512-XZ/lF9iqf9HXApIHQHqzJK5v2w4mkUMsVqAzOyWVzoiwwXEavY6Tpuw7HavgzIoD+huVff4JghSGcgEfX6eycg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.0", "@rc-component/async-validator": "^5.0.3", @@ -4915,6 +5613,7 @@ "version": "2.4.12", "resolved": "https://registry.npmjs.org/rc-form/-/rc-form-2.4.12.tgz", "integrity": "sha512-sHfyWRrnjCHkeCYfYAGop2GQBUC6CKMPcJF9h/gL/vTmZB/RN6fNOGKjXrXjFbwFwKXUWBoPtIDDDmXQW9xNdw==", + "license": "MIT", "dependencies": { "async-validator": "~1.11.3", "babel-runtime": "6.x", @@ -4934,6 +5633,7 @@ "version": "4.21.1", "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-4.21.1.tgz", "integrity": "sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg==", + "license": "MIT", "dependencies": { "add-dom-event-listener": "^1.1.0", "prop-types": "^15.5.10", @@ -4943,14 +5643,15 @@ } }, "node_modules/rc-image": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.6.0.tgz", - "integrity": "sha512-tL3Rvd1sS+frZQ01i+tkeUPaOeFz2iG9/scAt/Cfs0hyCRVA/w0Pu1J/JxIX8blalvmHE0bZQRYdOmRAzWu4Hg==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/rc-image/-/rc-image-7.11.0.tgz", + "integrity": "sha512-aZkTEZXqeqfPZtnSdNUnKQA0N/3MbgR7nUnZ+/4MfSFWPFHZau4p5r5ShaI0KPEMnNjv4kijSCFq/9wtJpwykw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/portal": "^1.0.2", "classnames": "^2.2.6", - "rc-dialog": "~9.4.0", + "rc-dialog": "~9.6.0", "rc-motion": "^2.6.2", "rc-util": "^5.34.1" }, @@ -4960,9 +5661,10 @@ } }, "node_modules/rc-input": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.5.1.tgz", - "integrity": "sha512-+nOzQJDeIfIpNP/SgY45LXSKbuMlp4Yap2y8c+ZpU7XbLmNzUd6+d5/S75sA/52jsVE6S/AkhkkDEAOjIu7i6g==", + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/rc-input/-/rc-input-1.6.3.tgz", + "integrity": "sha512-wI4NzuqBS8vvKr8cljsvnTUqItMfG1QbJoxovCgL+DX4eVUcHIjVwharwevIxyy7H/jbLryh+K7ysnJr23aWIA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -4974,14 +5676,15 @@ } }, "node_modules/rc-input-number": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.1.0.tgz", - "integrity": "sha512-NqJ6i25Xn/AgYfVxynlevIhX3FuKlMwIFpucGG1h98SlK32wQwDK0zhN9VY32McOmuaqzftduNYWWooWz8pXQA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/rc-input-number/-/rc-input-number-9.2.0.tgz", + "integrity": "sha512-5XZFhBCV5f9UQ62AZ2hFbEY8iZT/dm23Q1kAg0H8EvOgD3UDbYYJAayoVIkM3lQaCqYAW5gV0yV3vjw1XtzWHg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", "classnames": "^2.2.5", - "rc-input": "~1.5.0", + "rc-input": "~1.6.0", "rc-util": "^5.40.1" }, "peerDependencies": { @@ -4990,16 +5693,17 @@ } }, "node_modules/rc-mentions": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.13.1.tgz", - "integrity": "sha512-DSyUDq/PPCleUX1eghIn371lTSRQsIuCs1N7xR9nZcHP9R1NkE7JjpWUP8Gy4EGVPu0JN0qIcokxYJaoGPnofg==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/rc-mentions/-/rc-mentions-2.16.1.tgz", + "integrity": "sha512-GnhSTGP9Mtv6pqFFGQze44LlrtWOjHNrUUAcsdo9DnNAhN4pwVPEWy4z+2jpjkiGlJ3VoXdvMHcNDQdfI9fEaw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.22.5", "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.6", - "rc-input": "~1.5.0", - "rc-menu": "~9.14.0", - "rc-textarea": "~1.7.0", + "rc-input": "~1.6.0", + "rc-menu": "~9.15.1", + "rc-textarea": "~1.8.0", "rc-util": "^5.34.1" }, "peerDependencies": { @@ -5008,9 +5712,10 @@ } }, "node_modules/rc-menu": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.14.0.tgz", - "integrity": "sha512-La3LBCDMLMs9Q/8mTGbnscb+ZeJ26ebkLz9xJFHd2SD8vfsCKl1Z/k3mwbxyKL01lB40fel1s9Nn9LAv/nmVJQ==", + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/rc-menu/-/rc-menu-9.15.1.tgz", + "integrity": "sha512-UKporqU6LPfHnpPmtP6hdEK4iO5Q+b7BRv/uRpxdIyDGplZy9jwUjsnpev5bs3PQKB0H0n34WAPDfjAfn3kAPA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.0.0", @@ -5025,13 +5730,14 @@ } }, "node_modules/rc-motion": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.1.tgz", - "integrity": "sha512-QD4bUqByjVQs7PhUT1d4bNxvtTcK9ETwtg7psbDfo6TmYalH/1hhjj4r2hbhW7g5OOEqYHhfwfj4noIvuOVRtQ==", + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/rc-motion/-/rc-motion-2.9.3.tgz", + "integrity": "sha512-rkW47ABVkic7WEB0EKJqzySpvDqwl60/tdkY7hWP7dYnh5pm0SzJpo54oW3TDUGXV5wfxXFmMkxrzRRbotQ0+w==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", - "rc-util": "^5.39.3" + "rc-util": "^5.43.0" }, "peerDependencies": { "react": ">=16.9.0", @@ -5039,9 +5745,10 @@ } }, "node_modules/rc-notification": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.4.0.tgz", - "integrity": "sha512-li19y9RoYJciF3WRFvD+DvWS70jdL8Fr+Gfb/OshK+iY6iTkwzoigmSIp76/kWh5tF5i/i9im12X3nsF85GYdA==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/rc-notification/-/rc-notification-5.6.2.tgz", + "integrity": "sha512-Id4IYMoii3zzrG0lB0gD6dPgJx4Iu95Xu0BQrhHIbp7ZnAZbLqdqQ73aIWH0d0UFcElxwaKjnzNovTjo7kXz7g==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -5060,6 +5767,7 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/rc-overflow/-/rc-overflow-1.3.2.tgz", "integrity": "sha512-nsUm78jkYAoPygDAcGZeC2VwIg/IBGSodtOY3pMof4W3M9qRJgqaDYm03ZayHlde3I6ipliAxbN0RUcGf5KOzw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -5072,9 +5780,10 @@ } }, "node_modules/rc-pagination": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-4.0.4.tgz", - "integrity": "sha512-GGrLT4NgG6wgJpT/hHIpL9nELv27A1XbSZzECIuQBQTVSf4xGKxWr6I/jhpRPauYEWEbWVw22ObG6tJQqwJqWQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/rc-pagination/-/rc-pagination-4.3.0.tgz", + "integrity": "sha512-UubEWA0ShnroQ1tDa291Fzw6kj0iOeF26IsUObxYTpimgj4/qPCWVFl18RLZE+0Up1IZg0IK4pMn6nB3mjvB7g==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", @@ -5086,16 +5795,17 @@ } }, "node_modules/rc-picker": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.5.0.tgz", - "integrity": "sha512-suqz9bzuhBQlf7u+bZd1bJLPzhXpk12w6AjQ9BTPTiFwexVZgUKViG1KNLyfFvW6tCUZZK0HmCCX7JAyM+JnCg==", + "version": "4.6.15", + "resolved": "https://registry.npmjs.org/rc-picker/-/rc-picker-4.6.15.tgz", + "integrity": "sha512-OWZ1yrMie+KN2uEUfYCfS4b2Vu6RC1FWwNI0s+qypsc3wRt7g+peuZKVIzXCTaJwyyZruo80+akPg2+GmyiJjw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.10.1", + "@babel/runtime": "^7.24.7", "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.1", "rc-overflow": "^1.3.2", "rc-resize-observer": "^1.4.0", - "rc-util": "^5.38.1" + "rc-util": "^5.43.0" }, "engines": { "node": ">=8.x" @@ -5127,6 +5837,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/rc-progress/-/rc-progress-4.0.0.tgz", "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.6", @@ -5138,9 +5849,10 @@ } }, "node_modules/rc-rate": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.12.0.tgz", - "integrity": "sha512-g092v5iZCdVzbjdn28FzvWebK2IutoVoiTeqoLTj9WM7SjA/gOJIw5/JFZMRyJYYVe1jLAU2UhAfstIpCNRozg==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/rc-rate/-/rc-rate-2.13.0.tgz", + "integrity": "sha512-oxvx1Q5k5wD30sjN5tqAyWTvJfLNNJn7Oq3IeS4HxWfAiC4BOXMITNAsw7u/fzdtO4MS8Ki8uRLOzcnEuoQiAw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", @@ -5158,6 +5870,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/rc-resize-observer/-/rc-resize-observer-1.4.0.tgz", "integrity": "sha512-PnMVyRid9JLxFavTjeDXEXo65HCRqbmLBw9xX9gfC4BZiSzbLXKzW3jPz+J0P71pLbD5tBMTT+mkstV5gD0c9Q==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.7", "classnames": "^2.2.1", @@ -5170,9 +5883,10 @@ } }, "node_modules/rc-segmented": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.3.0.tgz", - "integrity": "sha512-I3FtM5Smua/ESXutFfb8gJ8ZPcvFR+qUgeeGFQHBOvRiRKyAk4aBE5nfqrxXx+h8/vn60DQjOt6i4RNtrbOobg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/rc-segmented/-/rc-segmented-2.5.0.tgz", + "integrity": "sha512-B28Fe3J9iUFOhFJET3RoXAPFJ2u47QvLSYcZWC4tFYNGPEjug5LAxEasZlA/PpAxhdOPqGWsGbSj7ftneukJnw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", @@ -5185,9 +5899,10 @@ } }, "node_modules/rc-select": { - "version": "14.14.0", - "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.14.0.tgz", - "integrity": "sha512-Uo2wulrjoPPRLCPd7zlK4ZFVJxlTN//yp1xWP/U+TUOQCyXrT+Duvq/Si5OzVcmQyWAUSbsplc2OwNNhvbOeKQ==", + "version": "14.15.2", + "resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.15.2.tgz", + "integrity": "sha512-oNoXlaFmpqXYcQDzcPVLrEqS2J9c+/+oJuGrlXeVVX/gVgrbHa5YcyiRUXRydFjyuA7GP3elRuLF7Y3Tfwltlw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.1.1", @@ -5206,9 +5921,10 @@ } }, "node_modules/rc-slider": { - "version": "10.6.2", - "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.6.2.tgz", - "integrity": "sha512-FjkoFjyvUQWcBo1F3RgSglky3ar0+qHLM41PlFVYB4Bj3RD8E/Mv7kqMouLFBU+3aFglMzzctAIWRwajEuueSw==", + "version": "11.1.7", + "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-11.1.7.tgz", + "integrity": "sha512-ytYbZei81TX7otdC0QvoYD72XSlxvTihNth5OeZ6PMXyEDq/vHdWFulQmfDGyXK1NwKwSlKgpvINOa88uT5g2A==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", @@ -5226,6 +5942,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/rc-steps/-/rc-steps-6.0.1.tgz", "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.16.7", "classnames": "^2.2.3", @@ -5243,6 +5960,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-4.1.0.tgz", "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.21.0", "classnames": "^2.2.1", @@ -5254,15 +5972,16 @@ } }, "node_modules/rc-table": { - "version": "7.45.7", - "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.45.7.tgz", - "integrity": "sha512-wi9LetBL1t1csxyGkMB2p3mCiMt+NDexMlPbXHvQFmBBAsMxrgNSAPwUci2zDLUq9m8QdWc1Nh8suvrpy9mXrg==", + "version": "7.47.5", + "resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.47.5.tgz", + "integrity": "sha512-fzq+V9j/atbPIcvs3emuclaEoXulwQpIiJA6/7ey52j8+9cJ4P8DGmp4YzfUVDrb3qhgedcVeD6eRgUrokwVEQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/context": "^1.4.0", "classnames": "^2.2.5", "rc-resize-observer": "^1.1.0", - "rc-util": "^5.37.0", + "rc-util": "^5.41.0", "rc-virtual-list": "^3.14.2" }, "engines": { @@ -5274,14 +5993,15 @@ } }, "node_modules/rc-tabs": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.1.0.tgz", - "integrity": "sha512-xTNz4Km1025emtkv1q7xKhjPwAtXr/wycuXVTAcFJg+DKhnPDDbnwbA9KRW0SawAVOGvVEj8ZrBlU0u0FGLrbg==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/rc-tabs/-/rc-tabs-15.3.0.tgz", + "integrity": "sha512-lzE18r+zppT/jZWOAWS6ntdkDUKHOLJzqMi5UAij1LeKwOaQaupupAoI9Srn73GRzVpmGznkECMRrzkRusC40A==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.2", "classnames": "2.x", "rc-dropdown": "~4.2.0", - "rc-menu": "~9.14.0", + "rc-menu": "~9.15.1", "rc-motion": "^2.6.2", "rc-resize-observer": "^1.0.0", "rc-util": "^5.34.1" @@ -5295,13 +6015,14 @@ } }, "node_modules/rc-textarea": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.7.0.tgz", - "integrity": "sha512-UxizYJkWkmxP3zofXgc487QiGyDmhhheDLLjIWbFtDmiru1ls30KpO8odDaPyqNUIy9ugj5djxTEuezIn6t3Jg==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/rc-textarea/-/rc-textarea-1.8.2.tgz", + "integrity": "sha512-UFAezAqltyR00a8Lf0IPAyTd29Jj9ee8wt8DqXyDMal7r/Cg/nDt3e1OOv3Th4W6mKaZijjgwuPXhAfVNTN8sw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1", - "rc-input": "~1.5.0", + "rc-input": "~1.6.0", "rc-resize-observer": "^1.0.0", "rc-util": "^5.27.0" }, @@ -5311,9 +6032,10 @@ } }, "node_modules/rc-tooltip": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.2.0.tgz", - "integrity": "sha512-iS/3iOAvtDh9GIx1ulY7EFUXUtktFccNLsARo3NPgLf0QW9oT0w3dA9cYWlhqAKmD+uriEwdWz1kH0Qs4zk2Aw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rc-tooltip/-/rc-tooltip-6.2.1.tgz", + "integrity": "sha512-rws0duD/3sHHsD905Nex7FvoUGy2UBQRhTkKxeEvr2FB+r21HsOxcDJI0TzyO8NHhnAA8ILr8pfbSBg5Jj5KBg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/trigger": "^2.0.0", @@ -5325,9 +6047,10 @@ } }, "node_modules/rc-tree": { - "version": "5.8.7", - "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.8.7.tgz", - "integrity": "sha512-cpsIQZ4nNYwpj6cqPRt52e/69URuNdgQF9wZ10InmEf8W3+i0A41OVmZWwHuX9gegQSqj+DPmaDkZFKQZ+ZV1w==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/rc-tree/-/rc-tree-5.9.0.tgz", + "integrity": "sha512-CPrgOvm9d/9E+izTONKSngNzQdIEjMox2PBufWjS1wf7vxtvmCWzK1SlpHbRY6IaBfJIeZ+88RkcIevf729cRg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", @@ -5344,14 +6067,15 @@ } }, "node_modules/rc-tree-select": { - "version": "5.21.0", - "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.21.0.tgz", - "integrity": "sha512-w+9qEu6zh0G3wt9N/hzWNSnqYH1i9mH1Nqxo0caxLRRFXF5yZWYmpCDoDTMdQM1Y4z3Q5yj08qyrPH/d4AtumA==", + "version": "5.23.0", + "resolved": "https://registry.npmjs.org/rc-tree-select/-/rc-tree-select-5.23.0.tgz", + "integrity": "sha512-aQGi2tFSRw1WbXv0UVXPzHm09E0cSvUVZMLxQtMv3rnZZpNmdRXWrnd9QkLNlVH31F+X5rgghmdSFF3yZW0N9A==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", - "rc-select": "~14.14.0", - "rc-tree": "~5.8.1", + "rc-select": "~14.15.0", + "rc-tree": "~5.9.0", "rc-util": "^5.16.1" }, "peerDependencies": { @@ -5360,9 +6084,10 @@ } }, "node_modules/rc-upload": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.5.2.tgz", - "integrity": "sha512-QO3ne77DwnAPKFn0bA5qJM81QBjQi0e0NHdkvpFyY73Bea2NfITiotqJqVjHgeYPOJu5lLVR32TNGP084aSoXA==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/rc-upload/-/rc-upload-4.8.1.tgz", + "integrity": "sha512-toEAhwl4hjLAI1u8/CgKWt30BR06ulPa4iGQSMvSXoHzO88gPCslxqV/mnn4gJU7PDoltGIC9Eh+wkeudqgHyw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", "classnames": "^2.2.5", @@ -5374,9 +6099,10 @@ } }, "node_modules/rc-util": { - "version": "5.41.0", - "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.41.0.tgz", - "integrity": "sha512-xtlCim9RpmVv0Ar2Nnc3WfJCxjQkTf3xHPWoFdjp1fSs2NirQwqiQrfqdU9HUe0kdfb168M/T8Dq0IaX50xeKg==", + "version": "5.43.0", + "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.43.0.tgz", + "integrity": "sha512-AzC7KKOXFqAdIBqdGWepL9Xn7cm3vnAmjlHqUnoQaTMZYhM4VlXGLkkHHxj/BZ7Td0+SOPKB4RGPboBVKT9htw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" @@ -5389,12 +6115,14 @@ "node_modules/rc-util/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" }, "node_modules/rc-virtual-list": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.14.2.tgz", - "integrity": "sha512-rA+W5xryhklJAcmswNyuKB3ZGeB855io+yOFQK5u/RXhjdshGblfKpNkQr4/9fBhZns0+uiL/0/s6IP2krtSmg==", + "version": "3.14.8", + "resolved": "https://registry.npmjs.org/rc-virtual-list/-/rc-virtual-list-3.14.8.tgz", + "integrity": "sha512-8D0KfzpRYi6YZvlOWIxiOm9BGt4Wf2hQyEaM6RXlDDiY2NhLheuYI+RA+7ZaZj1lq+XQqy3KHlaeeXQfzI5fGg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.20.0", "classnames": "^2.2.6", @@ -5413,6 +6141,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -5424,6 +6153,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5435,22 +6165,26 @@ "node_modules/react-fast-compare": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", - "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", + "license": "MIT" }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" }, "node_modules/react-query": { "version": "3.39.3", "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", "integrity": "sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.5.5", "broadcast-channel": "^3.4.1", @@ -5477,16 +6211,18 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-router": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz", - "integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz", + "integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.16.1" + "@remix-run/router": "1.20.0" }, "engines": { "node": ">=14.0.0" @@ -5496,12 +6232,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.23.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz", - "integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==", + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz", + "integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==", + "license": "MIT", "dependencies": { - "@remix-run/router": "1.16.1", - "react-router": "6.23.1" + "@remix-run/router": "1.20.0", + "react-router": "6.27.0" }, "engines": { "node": ">=14.0.0" @@ -5515,6 +6252,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", + "license": "MIT", "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", @@ -5529,6 +6267,7 @@ "version": "16.1.34", "resolved": "https://registry.npmjs.org/react-svg/-/react-svg-16.1.34.tgz", "integrity": "sha512-L4ak1qNFLgzVbHm0xQEpHoIOqb3um/B0ybahd3U2TKoGZxb0JaPVI5lsAhvSng2P1kcsYEok2Z7RpcKx7arJGw==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.24.1", "@tanem/svg-injector": "^10.1.68", @@ -5541,12 +6280,14 @@ } }, "node_modules/react-syntax-highlighter": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", - "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.27.0", "refractor": "^3.6.0" @@ -5559,6 +6300,7 @@ "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -5575,6 +6317,7 @@ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", "dev": true, + "license": "MIT", "dependencies": { "pify": "^2.3.0" } @@ -5584,25 +6327,29 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dependencies": { - "picomatch": "^2.2.1" - }, + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "license": "MIT", "engines": { - "node": ">=8.10.0" + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/recharts": { - "version": "2.13.0-alpha.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0-alpha.4.tgz", - "integrity": "sha512-K9naL6F7pEcDYJE6yFQASSCQecSLPP0JagnvQ9hPtA/aHgsxsnIOjouLP5yrFZehxzfCkV5TEORr7/uNtSr7Qw==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.13.0.tgz", + "integrity": "sha512-sbfxjWQ+oLWSZEWmvbq/DFVdeRLqqA6d0CDjKx2PkxVVdoXo16jvENCE+u/x7HxOO+/fwx//nYRwb8p8X6s/lQ==", + "license": "MIT", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", @@ -5625,6 +6372,7 @@ "version": "0.4.5", "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", "dependencies": { "decimal.js-light": "^2.4.1" } @@ -5632,12 +6380,14 @@ "node_modules/recharts/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" }, "node_modules/refractor": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", "dependencies": { "hastscript": "^6.0.0", "parse-entities": "^2.0.0", @@ -5652,6 +6402,7 @@ "version": "1.27.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -5659,23 +6410,27 @@ "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" }, "node_modules/remove-accents": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", - "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==", + "license": "MIT" }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -5693,6 +6448,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -5702,6 +6458,7 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -5712,6 +6469,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -5723,12 +6481,13 @@ } }, "node_modules/rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -5738,22 +6497,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", "fsevents": "~2.3.2" } }, @@ -5776,6 +6535,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -5784,14 +6544,17 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT", "optional": true }, "node_modules/sass": { - "version": "1.77.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.4.tgz", - "integrity": "sha512-vcF3Ckow6g939GMA4PeU7b2K/9FALXk2KF9J87txdHzXbUF9XRQRwSxcAs/fGaTnJeBFd7UoV22j3lzMLdM0Pw==", + "version": "1.80.3", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.3.tgz", + "integrity": "sha512-ptDWyVmDMVielpz/oWy3YP3nfs7LpJTHIJZboMVs8GEC9eUmtZTZhMHlTW98wY4aEorDfjN38+Wr/XjskFWcfA==", + "license": "MIT", "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, @@ -5806,12 +6569,14 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC", "optional": true }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } @@ -5820,15 +6585,17 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "license": "MIT", "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -5839,13 +6606,15 @@ "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5858,6 +6627,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5867,6 +6637,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -5879,6 +6650,7 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5887,6 +6659,7 @@ "version": "1.6.6", "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "license": "MIT", "engines": { "node": ">=8.0.0" } @@ -5895,15 +6668,17 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "optional": true, "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -5912,6 +6687,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5920,13 +6696,15 @@ "node_modules/string-convert": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", - "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" }, "node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -5945,6 +6723,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -5958,13 +6737,15 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -5977,6 +6758,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -5992,6 +6774,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -6005,6 +6788,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -6017,6 +6801,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -6025,15 +6810,17 @@ } }, "node_modules/stylis": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", - "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==" + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz", + "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==", + "license": "MIT" }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -6052,32 +6839,32 @@ } }, "node_modules/sucrase/node_modules/glob": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", - "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/sucrase/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -6093,6 +6880,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -6105,6 +6893,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -6113,10 +6902,11 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", - "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.14.tgz", + "integrity": "sha512-IcSvOcTRcUtQQ7ILQL5quRDg7Xs93PdJEk1ZLbhhvJc7uj/OAhYOnruEiwnGgBvUtaUAJ8/mhSw1o8L2jCiENA==", "dev": true, + "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -6149,17 +6939,70 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0" } @@ -6169,6 +7012,7 @@ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, + "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -6177,9 +7021,10 @@ } }, "node_modules/throttle-debounce": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", - "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", "engines": { "node": ">=12.22" } @@ -6187,23 +7032,27 @@ "node_modules/tiny-case": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", - "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==" + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "license": "MIT" }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -6212,6 +7061,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -6222,23 +7072,27 @@ "node_modules/toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" }, "node_modules/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT" }, "node_modules/trendline": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trendline/-/trendline-1.0.0.tgz", - "integrity": "sha512-eRSzayxbb65f6r9OITF89T0UjyOQvfbA7t5vefxvAE2GV44gDmENbon47zRlgy1i2RhMjwH7OQVUaJcOyB9MFQ==" + "integrity": "sha512-eRSzayxbb65f6r9OITF89T0UjyOQvfbA7t5vefxvAE2GV44gDmENbon47zRlgy1i2RhMjwH7OQVUaJcOyB9MFQ==", + "license": "MIT" }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -6250,18 +7104,21 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==", + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -6273,6 +7130,7 @@ "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" }, @@ -6281,10 +7139,11 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6294,15 +7153,17 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -6311,15 +7172,16 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", "integrity": "sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==", + "license": "Apache-2.0", "dependencies": { "@babel/runtime": "^7.6.2", "detect-node": "^2.0.4" } }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -6335,9 +7197,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -6351,6 +7214,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -6359,6 +7223,7 @@ "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "license": "MIT", "dependencies": { "inherits": "2.0.3" } @@ -6367,17 +7232,20 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/util/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "license": "ISC" }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", @@ -6396,14 +7264,15 @@ } }, "node_modules/vite": { - "version": "5.2.12", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", - "integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==", + "version": "5.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", + "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -6422,6 +7291,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -6439,6 +7309,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -6454,6 +7327,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" } @@ -6463,6 +7337,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -6478,6 +7353,7 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6487,6 +7363,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -6505,6 +7382,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -6522,6 +7400,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -6537,6 +7416,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -6548,19 +7428,22 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -6571,10 +7454,11 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6587,6 +7471,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6599,6 +7484,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -6612,12 +7498,14 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", "engines": { "node": ">=0.4" } @@ -6626,13 +7514,15 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yaml": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", - "integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", + "integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", "dev": true, + "license": "ISC", "bin": { "yaml": "bin.mjs" }, @@ -6645,6 +7535,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -6656,6 +7547,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/yup/-/yup-1.4.0.tgz", "integrity": "sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==", + "license": "MIT", "dependencies": { "property-expr": "^2.0.5", "tiny-case": "^1.0.3", diff --git a/ui/package.json b/ui/package.json index 7f3271cb..a65b3ea5 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,7 +1,7 @@ { - "name": "prompt-sail-web", + "name": "promptsail-ui", "private": true, - "version": "0.0.0", + "version": "0.5.4", "type": "module", "scripts": { "start": "vite", @@ -16,7 +16,7 @@ "@azure/msal-browser": "^3.13.0", "@react-oauth/google": "^0.12.1", "antd": "^5.17.3", - "axios": "^1.6.2", + "axios": "^1.7.4", "d3-scale-chromatic": "^3.1.0", "dotenv": "^16.3.1", "formik": "^2.4.5", @@ -32,7 +32,7 @@ "react-svg": "^16.1.32", "react-syntax-highlighter": "^15.5.0", "recharts": "^2.13.0-alpha.4", - "sass": "^1.69.5", + "sass": "^1.80.3", "slugify": "^1.6.6", "trendline": "^1.0.0", "yup": "^1.3.2" @@ -53,6 +53,6 @@ "postcss": "^8.4.31", "tailwindcss": "^3.3.5", "typescript": "^5.2.2", - "vite": "^5.0.0" + "vite": "^5.4.6" } } diff --git a/ui/src/hooks/useGetRangeDatesAndGranularity.tsx b/ui/src/hooks/useGetRangeDatesAndGranularity.tsx index 6ce320ad..c6f2425c 100644 --- a/ui/src/hooks/useGetRangeDatesAndGranularity.tsx +++ b/ui/src/hooks/useGetRangeDatesAndGranularity.tsx @@ -50,15 +50,15 @@ const useGetRangeDatesAndGranularity = ( options.push(Period.Weekly); break; case 3: // day - if (end.diff(start, 'd', true) >= 1 && end.diff(start, 'M', true) <= 2) + if (end.diff(start, 'd', true) >= 1 && end.diff(start, 'M', true) <= 3) // Changed from 2 to 3 months options.push(Period.Daily); break; case 4: // hour - if (end.diff(start, 'h', true) >= 1 && end.diff(start, 'h', true) <= 60) + if (end.diff(start, 'h', true) >= 1 && end.diff(start, 'h', true) <= 120) options.push(Period.Hourly); break; case 5: // minutes - if (end.diff(start, 'h', true) <= 5) options.push(Period.Minutely); + if (end.diff(start, 'h', true) <= 12) options.push(Period.Minutely); break; default: break; @@ -114,44 +114,32 @@ const useGetRangeDatesAndGranularity = ( allowClear={false} allowEmpty={false} presets={[ - { - label: 'Last 30 minutes', - value: [dayjs().add(-0.5, 'h'), dayjs()] - }, - { - label: 'Last hour', - value: [dayjs().add(-1, 'h'), dayjs()] - }, - { - label: 'Last 6 hours', - value: [dayjs().add(-6, 'h'), dayjs()] - }, { label: 'Last 12 hours', - value: [dayjs().add(-12, 'h'), dayjs()] + value: [dayjs().subtract(12, 'hour'), dayjs()] }, { - label: 'Today', - value: [dayjs().startOf('day'), dayjs()] + label: 'Last 24 hours', + value: [dayjs().subtract(24, 'hour'), dayjs()] }, { label: 'Yesterday', value: [ - dayjs().add(-1, 'd').startOf('day'), - dayjs().add(0, 'd').startOf('day') + dayjs().subtract(1, 'day').startOf('day'), + dayjs().subtract(1, 'day').endOf('day') ] }, { label: 'Last 7 Days', - value: [dayjs().add(-7, 'd').startOf('day'), dayjs()] + value: [dayjs().subtract(7, 'day').startOf('day'), dayjs()] }, { - label: 'Last 14 Days', - value: [dayjs().add(-14, 'd').startOf('day'), dayjs()] + label: 'Last 30 Days', + value: [dayjs().subtract(30, 'day').startOf('day'), dayjs()] }, { - label: 'Last 30 Days', - value: [dayjs().add(-30, 'd').startOf('day'), dayjs()] + label: 'Last 3 Months', + value: [dayjs().subtract(3, 'month').startOf('day'), dayjs()] } ]} /> diff --git a/ui/src/index.sass b/ui/src/index.sass index 120f950b..2c789e35 100644 --- a/ui/src/index.sass +++ b/ui/src/index.sass @@ -1,3 +1,7 @@ +@use 'sass:map' +@use './func' +@use './components/Sign/styles' + @import url('https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,100..1000&display=swap') @import url('https://fonts.googleapis.com/css2?family=Unica+One&display=swap') @@ -6,20 +10,16 @@ body overflow: hidden font-family: 'Roboto Flex' -@import './func.sass' - @tailwind base @tailwind components @tailwind utilities -@import './components/Sign/styles' - -$headers:("fontSize1": 38, "fontSize2": 30, "fontSize3": 24, "fontSize4": 20, "fontSize5": 16, "lineHeight1": 1.21053, "lineHeight2": 1.26667, "lineHeight3": 1.33333, "lineHeight4": 1.40, "lineHeight5": 1.5) +$headers: ("fontSize1": 38, "fontSize2": 30, "fontSize3": 24, "fontSize4": 20, "fontSize5": 16, "lineHeight1": 1.21053, "lineHeight2": 1.26667, "lineHeight3": 1.33333, "lineHeight4": 1.40, "lineHeight5": 1.5) @for $i from 1 through 5 .h#{$i} - font-size: #{map-get($headers, "fontSize#{$i}")} + px !important - line-height: #{map-get($headers, "lineHeight#{$i}")} !important + font-size: #{map.get($headers, "fontSize#{$i}")}px !important + line-height: #{map.get($headers, "lineHeight#{$i}")} !important .ant-table-body overflow-y: hidden !important @@ -75,4 +75,4 @@ div .ant-table-tbody td padding-left: 8px !important padding-right: 8px !important - \ No newline at end of file + diff --git a/ui/src/theme-light.tsx b/ui/src/theme-light.tsx index e241b4d2..cec240a5 100644 --- a/ui/src/theme-light.tsx +++ b/ui/src/theme-light.tsx @@ -64,7 +64,9 @@ const theme: ThemeConfig = { headerPadding: `${styles.global['Space/Padding/padding']}px ${styles.global['Space/Padding/paddingLG']}px` }, Layout: { - bodyBg: '#fafbfa' || styles.Colors.light['Fill/colorFillQuaternary'], + bodyBg: styles.Colors.light['Fill/colorFillQuaternary'] !== undefined + ? styles.Colors.light['Fill/colorFillQuaternary'] + : '#fafbfa', headerBg: styles.Colors.light['Background/colorBgContainer'], headerPadding: `${styles.global['Size/sizeMD']}px 24px`, siderBg: styles.Colors.light['Text/colorTextBase']