diff --git a/src/.env b/src/.env index 3018f839..a78e29dc 100644 --- a/src/.env +++ b/src/.env @@ -18,7 +18,7 @@ DOCKER_IMAGE_FRONTEND=pom_frontend # Backend PRIVATIZE_API= -TESTING=True +TESTING= RATE_LIMIT= BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,http://localhost:5174,https://localhost,https://localhost:5173,https://localhost:5174" PROJECT_NAME="pathofmodifiers" @@ -35,20 +35,15 @@ SMTP_USER=noreply@pathofmodifiers.com SMTP_PASSWORD=changethis TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA -# League -CURRENT_SOFTCORE_LEAGUE="Mirage" -ALL_SOFTCORE_LEAGUES="Mirage|Keepers|Mercenaries|Phrecia" - -LEAGUE_LAUNCH_TIME=2026-03-06T19:00:00Z # ISO 8601 format. Round backwards to whole hour number # Data retrieval MANUAL_NEXT_CHANGE_ID=True -# Change ID Retrieved 02.09.2024: -NEXT_CHANGE_ID=2621291179-2592930469-2516326916-2790039644-2710714910 +# Change ID Retrieved 28.06.2026: +NEXT_CHANGE_ID=3176140940-3103957281-3029294684-3370770619-3261964453 POE_PUBLIC_STASHES_AUTH_TOKEN=changethis # Enables the public stashes creation script in data_ret/app/tests/scripts. # Set value "True" to activate: -DATA_RET_TEST_PUB_STASH_SIM_DATA_DEPOSIT_ENABLED=True +DATA_RET_TEST_PUB_STASH_SIM_DATA_DEPOSIT_ENABLED= # Postgres POSTGRES_PORT=5432 diff --git a/src/backend_api/app/alembic/versions/965e766db0a0_added_league_table.py b/src/backend_api/app/alembic/versions/965e766db0a0_added_league_table.py new file mode 100644 index 00000000..7586677d --- /dev/null +++ b/src/backend_api/app/alembic/versions/965e766db0a0_added_league_table.py @@ -0,0 +1,267 @@ +"""Added league table + +Revision ID: 965e766db0a0 +Revises: 17daa1c96438 +Create Date: 2026-06-30 15:49:45.173827 + +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +from app.alembic.replaceable_objects.main import ReplaceableTrigger + +# revision identifiers, used by Alembic. +revision: str = "965e766db0a0" +down_revision: Union[str, None] = "17daa1c96438" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +unidentified_aggregation_trigger_old = ReplaceableTrigger( + "aggregate_unidentified", + "unidentified_item", + """ + RETURNS TRIGGER AS ${name}$ + DECLARE + current_hour INT; + divine_id INT; + divine_value FLOAT; + BEGIN + IF EXISTS (SELECT 1 FROM unidentified_item AS unid WHERE unid."createdHoursSinceLaunch" = NEW."createdHoursSinceLaunch") THEN + RETURN NEW; + END IF; + + current_hour := (SELECT MAX(unid."createdHoursSinceLaunch") FROM unidentified_item AS unid); + divine_id := (SELECT MAX(cur."currencyId") FROM currency AS cur WHERE cur."tradeName"='divine'); + divine_value := (SELECT cur."valueInChaos" FROM currency AS cur WHERE cur."currencyId"=divine_id); + + + WITH aggregates AS ( + SELECT name, unid."itemBaseTypeId", unid."createdHoursSinceLaunch", league, ilvl, stddev(unid."currencyAmount" * cur."valueInChaos") AS std, AVG(unid."currencyAmount" * cur."valueInChaos") AS calc_avg, COUNT(unid."itemId") AS calc_count + FROM unidentified_item AS unid + NATURAL JOIN currency as cur + WHERE unid."createdHoursSinceLaunch" = current_hour + AND NOT aggregated + GROUP BY name, unid."itemBaseTypeId", unid."createdHoursSinceLaunch", league, ilvl + ), affected_item_ids AS ( + SELECT unid."itemId" FROM unidentified_item AS unid WHERE NOT aggregated AND unid."createdHoursSinceLaunch"=current_hour + ) + + INSERT INTO unidentified_item (name, "itemBaseTypeId", "createdHoursSinceLaunch", league, "currencyId", ilvl, "currencyAmount", "nItems", identified, rarity, aggregated) + SELECT name, unid."itemBaseTypeId", unid."createdHoursSinceLaunch", league, divine_id, ilvl, AVG(unid."currencyAmount" * cur."valueInChaos") / divine_value, calc_count, identified, rarity, TRUE + FROM unidentified_item AS unid + NATURAL JOIN aggregates + NATURAL JOIN currency AS cur + WHERE unid."currencyAmount" * cur."valueInChaos" <= calc_avg + 1.97 * std + AND unid."currencyAmount" * cur."valueInChaos" >= calc_avg - 1.97 * std + GROUP BY name, unid."itemBaseTypeId", unid."createdHoursSinceLaunch", league, ilvl, identified, rarity, calc_count; + + DELETE FROM unidentified_item as unid + WHERE NOT aggregated AND unid."createdHoursSinceLaunch"=current_hour; + + RETURN NEW; + END; + ${name}$ LANGUAGE plpgsql; + """, + """ + BEFORE INSERT ON {table} + FOR EACH ROW + EXECUTE FUNCTION {name}(); + """, +) + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "league", + sa.Column( + "leagueId", sa.SmallInteger(), sa.Identity(always=False), nullable=False + ), + sa.Column("name", sa.Text(), nullable=False), + sa.Column("validFrom", sa.DateTime(timezone=True), nullable=False), + sa.Column("validTo", sa.DateTime(timezone=True), nullable=True), + sa.Column("version", sa.Float(), nullable=False), + sa.PrimaryKeyConstraint("leagueId"), + ) + op.execute(""" + INSERT INTO league (name, "validFrom", version) + SELECT DISTINCT + league, + TIMESTAMP '1970-01-01 00:00:00', + 0.0 + FROM item + WHERE league IS NOT NULL + """) + + # ---- Item table ---- + op.add_column("item", sa.Column("leagueId", sa.SmallInteger())) + op.execute(""" + UPDATE item + SET "leagueId" = l."leagueId" + FROM league l + WHERE item.league = l.name + """) + op.alter_column("item", "leagueId", nullable=False) + op.create_foreign_key( + "fk_item_league", + "item", + "league", + ["leagueId"], + ["leagueId"], + ondelete="RESTRICT", + onupdate="CASCADE", + ) + op.drop_column("item", "league") + + # ---- Unidentified item table ---- + op.add_column("unidentified_item", sa.Column("leagueId", sa.SmallInteger())) + op.execute(""" + UPDATE unidentified_item + SET "leagueId" = l."leagueId" + FROM league l + WHERE league = l.name + """) + op.alter_column("unidentified_item", "leagueId", nullable=False) + op.create_foreign_key( + "fk_unidentified_item_league", + "unidentified_item", + "league", + ["leagueId"], + ["leagueId"], + ondelete="RESTRICT", + onupdate="CASCADE", + ) + op.drop_column("unidentified_item", "league") + + # ---- Currency table ---- + op.add_column("currency", sa.Column("leagueId", sa.SmallInteger())) + # Deletes all currency ids that are not used + op.execute(""" + DELETE FROM currency c + WHERE NOT EXISTS ( + SELECT 1 + FROM item i + WHERE i."currencyId" = c."currencyId" + ) + """) + op.execute(""" + UPDATE currency c + SET "leagueId" = i.leagueId + FROM ( + SELECT item."currencyId", MIN(item."leagueId") AS leagueId + FROM item + GROUP BY item."currencyId" + ) i + WHERE c."currencyId" = i."currencyId" + """) + op.alter_column("currency", "leagueId", nullable=False) + op.create_foreign_key( + "fk_currency_league", + "currency", + "league", + ["leagueId"], + ["leagueId"], + ondelete="RESTRICT", + onupdate="CASCADE", + ) + + # ---- Indexes ---- + # Manual SQL to drop query instead of `op.drop_index` is needed due to hypertables + op.execute(""" + DROP INDEX IF EXISTS ix_item_name_itemBaseTypeId_createdHoursSinceLaunch_league + """) + op.create_index( + "ix_item_name_itemBaseTypeId_createdHoursSinceLaunch_leagueId", + "item", + [ + "name", + "itemBaseTypeId", + "createdHoursSinceLaunch", + "leagueId", + ], + unique=False, + ) + + op.execute(""" + DROP INDEX IF EXISTS ix_unid_item_name_itemBaseTypeId_createdHoursSinceLaunch_league + """) + op.create_index( + "ix_unid_name_itemBaseTypeId_createdHoursSinceLaunch_leagueId", + "unidentified_item", + [ + "name", + "itemBaseTypeId", + "createdHoursSinceLaunch", + "leagueId", + ], + unique=False, + ) + + # ---- triggers ---- + op.drop_trigger(unidentified_aggregation_trigger_old) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # ---- Item table ---- + op.add_column( + "item", + sa.Column("league", sa.TEXT(), autoincrement=False), + ) + op.execute(""" + UPDATE item i + SET league = l.name + FROM league l + WHERE i."leagueId" = l."leagueId" + """) + op.alter_column("item", "league", nullable=False) + op.drop_constraint("fk_item_league", "item", type_="foreignkey") + op.execute(""" + DROP INDEX IF EXISTS ix_item_name_itemBaseTypeId_createdHoursSinceLaunch_leagueId + """) + op.create_index( + "ix_item_name_itemBaseTypeId_createdHoursSinceLaunch_league", + "item", + ["name", "itemBaseTypeId", "createdHoursSinceLaunch", "league"], + unique=False, + ) + op.drop_column("item", "leagueId") + + # ---- Unidentified item table ---- + op.add_column( + "unidentified_item", + sa.Column("league", sa.TEXT(), autoincrement=False), + ) + op.execute(""" + UPDATE unidentified_item ui + SET league = l.name + FROM league l + WHERE ui."leagueId" = l."leagueId" + """) + op.alter_column("unidentified_item", "league", nullable=False) + op.drop_constraint( + "fk_unidentified_item_league", "unidentified_item", type_="foreignkey" + ) + op.execute(""" + DROP INDEX IF EXISTS ix_unid_name_itemBaseTypeId_createdHoursSinceLaunch_leagueId + """) + op.create_index( + "ix_unid_item_name_itemBaseTypeId_createdHoursSinceLaunch_league", + "unidentified_item", + ["name", "itemBaseTypeId", "createdHoursSinceLaunch", "league"], + unique=False, + ) + op.drop_column("unidentified_item", "leagueId") + + # ---- Currency table ---- + op.drop_column("currency", "leagueId") + + op.drop_table("league") + + # ---- triggers ---- + op.create_trigger(unidentified_aggregation_trigger_old) + # ### end Alembic commands ### diff --git a/src/backend_api/app/api/README.md b/src/backend_api/app/api/README.md deleted file mode 100644 index 2602df6e..00000000 --- a/src/backend_api/app/api/README.md +++ /dev/null @@ -1,302 +0,0 @@ -# Currently outdated. Check out $DOMAIN/docs for updated contents - -## Table of contents -- [API documentation for POM](#api-documentation-for-pom) - - [/currency](#currency) - - [\[GET\] "/currency/{currencyId}" Get Currency](#get-currencycurrencyid-get-currency) - - [\[GET\] "/currency/" Get All Currencies](#get-currency-get-all-currencies) - - [\[POST\] "/currency/" Create Currency](#post-currency-create-currency) - - [\[PUT\] "/currency/{currencyId}" Update Currency](#put-currencycurrencyid-update-currency) - - [\[DELETE\] "/currency/{currencyId}" Delete Currency](#delete-currencycurrencyid-delete-currency) - - [/itemBaseType](#itembasetype) - - [\[GET\] "/itemBaseType/{baseType}" Get Item Base Type](#get-itembasetypebasetype-get-item-base-type) - - [\[GET\] "/itemBaseType/" Get All Item Base Types](#get-itembasetype-get-all-item-base-types) - - [\[POST\] "/itemBaseType/" Create Item Base Type](#post-itembasetype-create-item-base-type) - - [\[PUT\] "/itemBaseType/{baseType}" Update Item Base Type](#put-itembasetypebasetype-update-item-base-type) - - [\[DELETE\] "/itemBaseType/{baseType}" Delete Item Base Type](#delete-itembasetypebasetype-delete-item-base-type) - - [/itemModifier](#itemmodifier) - - [\[GET\] "/itemModifier/{itemId}" Get Item Modifier](#get-itemmodifieritemid-get-item-modifier) - - [\[GET\] "/itemModifier/" Get All Item Modifiers](#get-itemmodifier-get-all-item-modifiers) - - [\[POST\] "/itemModifier/" Create Item Modifier](#post-itemmodifier-create-item-modifier) - - [\[PUT\] "/itemModifier/{itemId}" Update Item Modifier](#put-itemmodifieritemid-update-item-modifier) - - [\[DELETE\] "/itemModifier/{itemId}" Delete Item Modifier](#delete-itemmodifieritemid-delete-item-modifier) - - [/item](#item) - - [\[GET\] "/item/{itemId}" Get Item](#get-itemitemid--get-item) - - [\[GET\] "/item/" Get All Items](#get-item-get-all-items) - - [\[POST\] "/item/" Create Item](#post-item-create-item) - - [\[PUT\] "/item/{itemId}" Update Item](#put-itemitemid-update-item) - - [\[DELETE\] "/item/{itemId}" Delete Item](#delete-itemitemid-delete-item) - - [/modifier](#modifier) - - [\[GET\] "/modifier/{modifierId}" Get Modifier](#get-modifiermodifierid--get-modifier) - - [\[GET\] "/modifier/" Get All Modifiers](#get-modifier-get-all-modifiers) - - [\[POST\] "/modifier/" Create Modifier](#post-modifier-create-modifier) - - [\[PUT\] "/modifier/{modifierId}" Update Modifier](#put-modifiermodifierid-update-modifier) - - [\[DELETE\] "/modifier/{modifierId}" Delete Modifier](#delete-modifiermodifierid-delete-modifier) - -# API documentation for POM - -This section covers the API functions for version 1 in the POM app. - -Each function serves one of five basic operations: retrieving specific data ``Get X``, retrieving all data ``Get All X``, creating data ``Create X``, updating data ``Update X``, and deleting data ``Delete X``. - -The ``Get X`` function retrieves an object by mapping to its primary key. Some primary key attributes are essential and can't be null, while others in the query can be nullable. For ``Get X``, if any or all of the non-essential attributes are null, the query fetches all objects with non-null values for the primary key. It's worth noting that this feature is currently limited to ``Get X`` and isn't available for ``Update X`` and ``Delete X``. However, these capabilities are planned for future releases. - - -## /currency - -### [GET] "/currency/{currencyId}" Get Currency - -Get currency by key and value for "currencyId". - -Always returns one currency. - -![get_currency](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/58422369-bc4c-4b25-ac14-7c90a21b8a10) - -### [GET] "/currency/" Get All Currencies - -Get all currencies. - -Returns a list of all currencies. - -![get_all_currencies](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/d7d7b131-2631-4d2e-ae10-aafa0e430f82) - -### [POST] "/currency/" Create Currency - -Create one or a list of currencies. - -Returns the created currency or list of currencies. - -![create_currency](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/c0daccb6-97e5-4f35-9954-6ff287ac3fce) - - -### [PUT] "/currency/{currencyId}" Update Currency - -Update a currency by key and value for "currencyId". - -Returns the updated currency. - -![update_currency](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/66822fa2-9325-42e2-b2ea-a1103f833fad) - - -### [DELETE] "/currency/{currencyId}" Delete Currency - -Delete a currency by key and value for "currencyId". - -Returns a message indicating the currency was deleted. - -Always deletes one currency. - -![delete_currency](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/1b84d3bf-7bdd-43a5-be06-2cd57149b72b) - - - - - -## /itemBaseType - -### [GET] "/itemBaseType/{baseType}" Get Item Base Type - -Get item base type by key and value for "baseType". - -Always returns one item base type. - -![get_item_base_type](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/0867c743-bc6e-4622-b95b-2ac225b91352) - -### [GET] "/itemBaseType/" Get All Item Base Types - -Get all item base types. - -Returns a list of all item base types. - -![get_all_item_base_types](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/f8e8e65b-1041-40b9-9b8f-97fd1087b0a1) - -### [POST] "/itemBaseType/" Create Item Base Type - -Create one or a list of new item base types. - -Returns the created item base type or list of item base types. - -![create_item_base_type](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/04fb935e-ee30-4def-9dfa-996b432da00e) - - -### [PUT] "/itemBaseType/{baseType}" Update Item Base Type - -Update an item base type by key and value for "baseType". - -Returns the updated item base type. - -![update_item_base_type](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/8c4b78bc-aaf0-47b0-a182-5e68bfe4d2d4) - - -### [DELETE] "/itemBaseType/{baseType}" Delete Item Base Type - -Delete an item base type by key and value for "baseType". - -Returns a message that the item base type was deleted successfully. - -Always deletes one item base type. - -![delete_item_base_type](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/315593bd-694c-4bea-9249-f2ed29bca0ce) - - - -## /itemModifier - -### [GET] "/itemModifier/{itemId}" Get Item Modifier - -Get item modifier or list of item modifiers by key and -value for "itemId", optional "modifierId" and optional "position". - -Dominant key is "itemId". - -Returns one or a list of item modifiers. - -![get_item_modifier](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/2eca9b4f-af3b-4b8b-bde5-ada32c338348) - -### [GET] "/itemModifier/" Get All Item Modifiers - -Get all item modifiers. - -Returns a list of all item modifiers. - -![get_all_item_modifiers](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/c378172c-6195-42eb-ab94-6828302454ca) - -### [POST] "/itemModifier/" Create Item Modifier - -Create one or a list item modifiers. - -Returns the created item modifier or list of item modifiers. - -![create_item_modifier](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/4f922a54-8545-4894-a21c-f88a4910f3bd) - -### [PUT] "/itemModifier/{itemId}" Update Item Modifier - -Update an item modifier by key and value for -"itemId", optional "modifierId" and optional "position". - -Dominant key is "itemId". - -Returns the updated item modifier. - -![update_item_modifier](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/f72a1144-efc1-4080-8d60-414e50d6e624) - -### [DELETE] "/itemModifier/{itemId}" Delete Item Modifier - -Delete an item modifier by key and value for -"itemId", optional "modifierId" and optional "position". - -Dominant key is "itemId". - -Returns a message that the item modifier was deleted successfully. - -Always deletes one item modifier. - -![delete_item_modifier](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/a9c951d8-7aa6-4b81-b53f-e6904ae3a807) - - - - -## /item - -### [GET] "/item/{itemId}" Get Item - -Get item by key and value for "itemId". - -Always returns one item. - -![get_item](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/08588a4b-cddc-4aaf-86ea-d0c8f0d48c78) - -### [GET] "/item/" Get All Items - -Get all items. - -Returns a list of all items. - -![get_all_items](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/b450097f-b75c-4e7a-9f10-62a23ff9573b) - -### [POST] "/item/" Create Item - -Create one or a list of new items. - -Returns the created item or list of items. - -![create_item](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/42404020-3e8d-454d-88c8-06e3cc07b88a) - - -### [PUT] "/item/{itemId}" Update Item - -Update an item by key and value for "itemId". - -Returns the updated item. - -![update_item](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/54ca0201-a0eb-4dad-86c4-f57eefe01321) - - -### [DELETE] "/item/{itemId}" Delete Item - -Delete an item by key and value for "itemId". - -Returns a message indicating the item was deleted. - -Always deletes one item. - -![delete_item](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/974f7267-b31b-45ab-88e4-de37fc90d856) - - - - -## /modifier - -### [GET] "/modifier/{modifierId}" Get Modifier - -Get modifier or list of modifiers by key and -value for "modifierId" and optional "position" - -Dominant key is "modifierId". - -Returns one or a list of modifiers. - -![get_modifier](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/73a009ee-cef7-450a-bdae-af8264f79a99) - -### [GET] "/modifier/" Get All Modifiers - -Get all modifiers. - -Returns a list of all modifiers. - -![get_all_modifiers](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/1de82b32-0673-4cd7-80a4-86e6f15a30e5) - -### [POST] "/modifier/" Create Modifier - -Create one or a list of new modifiers. - -Returns the created modifier or list of modifiers. - -![create_modifier](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/421d446c-7657-4877-b3b8-f644adc1ed78) - - -### [PUT] "/modifier/{modifierId}" Update Modifier - -Update a modifier by key and value for "modifierId" and "position". - -Dominant key is "modifierId". - -Returns the updated modifier. - -![update_modifier](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/e7e972f8-b78c-499c-9f96-14dba050f774) - - -### [DELETE] "/modifier/{modifierId}" Delete Modifier - -Delete a modifier by key and value for "modifierId" -and optional "position". - -Dominant key is "modifierId". - -Returns a message that the modifier was deleted. - -Always deletes one modifier. - -![delete_modifier](https://github.com/Ivareh/pathofmodifiersapp/assets/69577035/e906d3ef-d7ae-4154-90a2-c1d047262422) - - diff --git a/src/backend_api/app/api/api.py b/src/backend_api/app/api/api.py index 77862728..19175c6a 100644 --- a/src/backend_api/app/api/api.py +++ b/src/backend_api/app/api/api.py @@ -9,6 +9,8 @@ item_modifier, item_modifier_prefix, item_prefix, + league, + league_prefix, login, login_prefix, modifier, @@ -27,40 +29,37 @@ api_router.include_router( - currency.router, prefix=f"/{currency_prefix}", tags=[f"{currency_prefix}s"] + currency.router, prefix=f"/{currency_prefix}", tags=[currency_prefix] ) api_router.include_router( item_base_type.router, prefix=f"/{item_base_type_prefix}", - tags=[f"{item_base_type_prefix}s"], + tags=[item_base_type_prefix], ) api_router.include_router( item_modifier.router, prefix=f"/{item_modifier_prefix}", - tags=[f"{item_modifier_prefix}s"], + tags=[item_modifier_prefix], ) +api_router.include_router(item.router, prefix=f"/{item_prefix}", tags=[item_prefix]) api_router.include_router( - item.router, prefix=f"/{item_prefix}", tags=[f"{item_prefix}s"] + league.router, prefix=f"/{league_prefix}", tags=[league_prefix] ) api_router.include_router( unidentified_item.router, prefix=f"/{unidentified_item_prefix}", - tags=[f"{unidentified_item_prefix}s"], + tags=[unidentified_item_prefix], ) api_router.include_router( - modifier.router, prefix=f"/{modifier_prefix}", tags=[f"{modifier_prefix}s"] + modifier.router, prefix=f"/{modifier_prefix}", tags=[modifier_prefix] ) +api_router.include_router(plot.router, prefix=f"/{plot_prefix}", tags=[plot_prefix]) api_router.include_router( - plot.router, prefix=f"/{plot_prefix}", tags=[f"{plot_prefix}s"] -) -api_router.include_router( - turnstile.router, prefix=f"/{turnstile_prefix}", tags=[f"{turnstile_prefix}s"] -) -api_router.include_router( - test.router, prefix=f"/{test_prefix}", tags=[f"{test_prefix}s"] + turnstile.router, prefix=f"/{turnstile_prefix}", tags=[turnstile_prefix] ) +api_router.include_router(test.router, prefix=f"/{test_prefix}", tags=[test_prefix]) api_router.include_router( login.router, prefix=f"/{login_prefix}", - tags=[f"{login_prefix}s"], + tags=[login_prefix], ) diff --git a/src/backend_api/app/api/routes/__init__.py b/src/backend_api/app/api/routes/__init__.py index 1ddd5963..8cc7f8c0 100644 --- a/src/backend_api/app/api/routes/__init__.py +++ b/src/backend_api/app/api/routes/__init__.py @@ -9,3 +9,4 @@ from app.api.routes.turnstile import turnstile_prefix from app.api.routes.user import user_prefix from app.api.routes.test import test_prefix +from app.api.routes.league import league_prefix diff --git a/src/backend_api/app/api/routes/currency.py b/src/backend_api/app/api/routes/currency.py index 30f1c69b..d9e3ed03 100644 --- a/src/backend_api/app/api/routes/currency.py +++ b/src/backend_api/app/api/routes/currency.py @@ -1,7 +1,6 @@ from typing import Annotated from fastapi import APIRouter, Depends, Query, Request, Response -from sqlalchemy import text from sqlalchemy.orm import Session import app.core.schemas as schemas @@ -19,6 +18,7 @@ from app.core.rate_limit.rate_limiters import ( apply_user_rate_limits, ) +from app.core.schemas.currency import CurrencyQuery from app.crud import CRUD_currency router = APIRouter() @@ -100,15 +100,90 @@ async def get_latest_currency_id( db: Session = Depends(get_db), ): """ - Get the latest currencyId + Get the latest currencyId, returns 1 if table is empty Can only be used safely on an empty table or directly after an insertion. """ - result = db.execute(text("""SELECT MAX("currencyId") FROM currency""")).fetchone() - if result: - return int(result[0]) - else: - return 1 + return await CRUD_currency.get_latest_currency_id(db) + + +@router.get( + "/latest_hours/", + response_model=dict[int, int], + tags=["latest_hours"], + dependencies=[ + Depends(get_current_active_user), + ], +) +@apply_user_rate_limits( + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_SECOND, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_MINUTE, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_HOUR, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_DAY, +) +async def get_latest_hours( + league_ids: Annotated[list[int], Query(min_length=1)], + request: Request, # noqa: ARG001 + response: Response, # noqa: ARG001 + db: Session = Depends(get_db), +): + """ + Returns a dict mapping league id to the most recent hour relating to that league. Dict is empty if no currencies exist. + + Does not guarantee an entry for every league id. + """ + return await CRUD_currency.get_latest_hours(db, league_ids) + + +@router.get( + "/latest_currencies/", + response_model=list[schemas.Currency], + tags=["latest_currencies"], + dependencies=[ + Depends(get_current_active_user), + ], +) +@apply_user_rate_limits( + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_SECOND, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_MINUTE, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_HOUR, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_DAY, +) +async def get_latest_currencies( + league_ids: Annotated[list[int], Query(min_length=1)], + request: Request, # noqa: ARG001 + response: Response, # noqa: ARG001 + db: Session = Depends(get_db), +): + """ + Returns a list of currencies for all queried leagues (if data exists). + """ + return await CRUD_currency.get_latest_currencies(db, league_ids) + + +@router.post( + "/from_query/", + response_model=list[schemas.Currency], + dependencies=[ + Depends(get_current_active_user), + ], +) +@apply_user_rate_limits( + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_SECOND, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_MINUTE, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_HOUR, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_DAY, +) +async def get_currency_from_query( + request: Request, # noqa: ARG001 + response: Response, # noqa: ARG001 + query_list: list[CurrencyQuery], + db: Session = Depends(get_db), +): + """ + Returns a list of currencies that match any of the queries + """ + return await CRUD_currency.get_currency_from_query(db, query_list) @router.post( diff --git a/src/backend_api/app/api/routes/league.py b/src/backend_api/app/api/routes/league.py new file mode 100644 index 00000000..af8877bf --- /dev/null +++ b/src/backend_api/app/api/routes/league.py @@ -0,0 +1,155 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends, Query, Request, Response +from sqlalchemy.orm import Session + +import app.core.schemas as schemas +from app.api.deps import ( + get_current_active_superuser, + get_current_active_user, + get_db, +) +from app.api.params import FilterParams +from app.core.rate_limit.rate_limit_config import rate_limit_settings +from app.core.rate_limit.rate_limiters import ( + apply_ip_rate_limits, + apply_user_rate_limits, +) +from app.crud import CRUD_league + +router = APIRouter() + + +league_prefix = "league" + + +@router.get( + "/{leagueId}", + response_model=schemas.League, + dependencies=[ + Depends(get_current_active_user), + ], +) +@apply_user_rate_limits( + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_SECOND, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_MINUTE, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_HOUR, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_DAY, +) +async def get_league( + request: Request, # noqa: ARG001 + response: Response, # noqa: ARG001 + leagueId: int, + db: Session = Depends(get_db), +): + """ + Get league by key and value for "leagueId". + + Always returns one league. + """ + + league_map = {"leagueId": leagueId} + league = await CRUD_league.get(db=db, filter=league_map) + + return league + + +@router.get( + "/", + response_model=schemas.League | list[schemas.League], +) +@apply_ip_rate_limits( + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_SECOND, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_MINUTE, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_HOUR, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_DAY, +) +async def get_all_leagues( + request: Request, # noqa: ARG001 + response: Response, # noqa: ARG001 + filter_params: Annotated[FilterParams, Query()], + db: Session = Depends(get_db), +): + """ + Get all leagues. + + Returns a list of all leagues. + """ + + all_leagues = await CRUD_league.get(db=db, filter_params=filter_params) + + return all_leagues + + +@router.get( + f"/active_{league_prefix}/", + response_model=list[schemas.League], + dependencies=[ + Depends(get_current_active_user), + ], +) +@apply_user_rate_limits( + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_SECOND, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_MINUTE, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_HOUR, + rate_limit_settings.DEFAULT_USER_RATE_LIMIT_DAY, +) +async def get_active_leagues( + request: Request, # noqa: ARG001 + response: Response, # noqa: ARG001 + db: Session = Depends(get_db), +): + """ + Get leagues that are still valid/active + + Always returns a list, but it may be empty. + """ + + leagues = await CRUD_league.get_active_leagues(db=db) + + return leagues + + +@router.post( + "/", + response_model=schemas.LeagueCreate | list[schemas.LeagueCreate] | None, + dependencies=[Depends(get_current_active_superuser)], +) +async def create_league( + league: schemas.LeagueCreate | list[schemas.LeagueCreate], + return_nothing: bool | None = None, + db: Session = Depends(get_db), +): + """ + Create one or a list of new leagues. + + Returns the created league or list of leagues. + """ + + return await CRUD_league.create(db=db, obj_in=league, return_nothing=return_nothing) + + +@router.put( + "/{leagueId}", + response_model=schemas.League, + dependencies=[Depends(get_current_active_superuser)], +) +async def update_modifier( + leagueId: int, + league_update: schemas.LeagueUpdate, + db: Session = Depends(get_db), +): + """ + Update a league by key and value for "leagueId" + + Returns the updated league. + """ + + modifier_map = {"leagueId": leagueId} + + modifier = await CRUD_league.get( + db=db, + filter=modifier_map, + ) + + return await CRUD_league.update(db_obj=modifier, obj_in=league_update, db=db) diff --git a/src/backend_api/app/api/routes/modifier.py b/src/backend_api/app/api/routes/modifier.py index 7b32506a..cd83a6e2 100644 --- a/src/backend_api/app/api/routes/modifier.py +++ b/src/backend_api/app/api/routes/modifier.py @@ -149,9 +149,7 @@ async def update_modifier( db: Session = Depends(get_db), ): """ - Update a modifier by key and value for "modifierId" - - Dominant key is "modifierId". + Update a modifier by key and value for "modifierId" and "position" Returns the updated modifier. """ diff --git a/src/backend_api/app/api/routes/unidentified_item.py b/src/backend_api/app/api/routes/unidentified_item.py index 84ffbcb6..cc55c311 100644 --- a/src/backend_api/app/api/routes/unidentified_item.py +++ b/src/backend_api/app/api/routes/unidentified_item.py @@ -94,3 +94,38 @@ async def create_item( return await CRUD_unidentifiedItem.create( db=db, obj_in=item, return_nothing=return_nothing ) + + +@router.get( + "/non_aggregated/", + response_model=list[schemas.UnidentifiedItem], + dependencies=[Depends(get_current_active_superuser)], +) +async def get_non_aggregated( + db: Session = Depends(get_db), +): + """ + Get the non aggregated unidentified items for the last 10 recorded hours + (not necessarily the last 10 hours) + """ + + all_items = await CRUD_unidentifiedItem.get_non_aggregated(db=db) + + return all_items + + +@router.post( + "/add_aggregated/", + dependencies=[Depends(get_current_active_superuser)], +) +async def add_aggregated( + aggregated_objs: list[schemas.UnidentifiedItemCreate], + db: Session = Depends(get_db), +): + """ + Deletes the non aggregated unidentified items for the last 10 recorded hours + (not necessarily the last 10 hours) + And inserts the new items + """ + + await CRUD_unidentifiedItem.add_aggregated(db=db, aggregated_objs=aggregated_objs) diff --git a/src/backend_api/app/core/models/models.py b/src/backend_api/app/core/models/models.py index 0f5951bf..bbe6f325 100644 --- a/src/backend_api/app/core/models/models.py +++ b/src/backend_api/app/core/models/models.py @@ -24,6 +24,18 @@ from app.core.models.database import Base +class League(Base): + __tablename__ = "league" + + leagueId: Mapped[SmallInteger] = mapped_column( + SmallInteger, Identity(), primary_key=True + ) + name: Mapped[str] = mapped_column(Text, nullable=False) + validFrom: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + validTo: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=True) + version: Mapped[float] = mapped_column(Float, nullable=False) + + class Currency(Base): __tablename__ = "currency" @@ -31,6 +43,11 @@ class Currency(Base): createdHoursSinceLaunch: Mapped[int] = mapped_column(SmallInteger, nullable=False) valueInChaos: Mapped[float] = mapped_column(Float(4), nullable=False) tradeName: Mapped[str] = mapped_column(Text, nullable=False) + leagueId: Mapped[SmallInteger] = mapped_column( + SmallInteger, + ForeignKey("league.leagueId", ondelete="RESTRICT", onupdate="CASCADE"), + nullable=False, + ) class ItemBaseType(Base): @@ -61,8 +78,9 @@ class _ItemBase: nullable=False, ) createdHoursSinceLaunch: Mapped[int] = mapped_column(SmallInteger, nullable=False) - league: Mapped[str] = mapped_column( - Text, + leagueId: Mapped[SmallInteger] = mapped_column( + SmallInteger, + ForeignKey("league.leagueId", ondelete="RESTRICT", onupdate="CASCADE"), nullable=False, ) itemId: Mapped[int] = mapped_column( @@ -104,11 +122,11 @@ class Item(_ItemBase, Base): __table_args__ = ( Index( - "ix_item_name_itemBaseTypeId_createdHoursSinceLaunch_league", + "ix_item_name_itemBaseTypeId_createdHoursSinceLaunch_leagueId", "name", "itemBaseTypeId", "createdHoursSinceLaunch", - "league", + "leagueId", ), ) @@ -127,11 +145,11 @@ class UnidentifiedItem(_ItemBase, Base): __table_args__ = ( Index( - "ix_unid_item_name_itemBaseTypeId_createdHoursSinceLaunch_league", + "ix_unid_name_itemBaseTypeId_createdHoursSinceLaunch_leagueId", "name", "itemBaseTypeId", "createdHoursSinceLaunch", - "league", + "leagueId", ), CheckConstraint( """ diff --git a/src/backend_api/app/core/schemas/__init__.py b/src/backend_api/app/core/schemas/__init__.py index bc580562..4d69acff 100644 --- a/src/backend_api/app/core/schemas/__init__.py +++ b/src/backend_api/app/core/schemas/__init__.py @@ -1,43 +1,49 @@ "All schemas are imported here and then exported to the main file" -from .currency import Currency, CurrencyInDB, CurrencyCreate, CurrencyUpdate -from .modifier import ( - Modifier, - GroupedModifierByEffect, - ModifierInDB, - ModifierCreate, - ModifierUpdate, -) +from .currency import Currency, CurrencyCreate, CurrencyInDB, CurrencyUpdate +from .item import Item, ItemCreate, ItemInDB, ItemUpdate from .item_base_type import ( ItemBaseType, - ItemBaseTypeInDB, ItemBaseTypeCreate, + ItemBaseTypeInDB, ItemBaseTypeUpdate, ) from .item_modifier import ( ItemModifier, - ItemModifierInDB, ItemModifierCreate, + ItemModifierInDB, ItemModifierUpdate, ) -from .item import Item, ItemInDB, ItemCreate, ItemUpdate +from .league import ( + League, + LeagueCreate, + LeagueInDB, + LeagueUpdate, +) +from .message import Message +from .modifier import ( + GroupedModifierByEffect, + Modifier, + ModifierCreate, + ModifierInDB, + ModifierUpdate, +) +from .token import NewPassword, Token, TokenPayload +from .turnstile import TurnstileQuery, TurnstileResponse from .unidentified_item import ( UnidentifiedItem, - UnidentifiedItemInDB, UnidentifiedItemCreate, + UnidentifiedItemInDB, UnidentifiedItemUpdate, ) -from .turnstile import TurnstileQuery, TurnstileResponse from .user import ( + UpdatePassword, + User, UserCreate, - UserUpdate, + UserInDB, + UserPublic, UserRegisterPreEmailConfirmation, UsersPublic, - UserPublic, + UserUpdate, UserUpdateMe, - UpdatePassword, - User, - UserInDB, ) -from .token import Token, TokenPayload, NewPassword -from .message import Message diff --git a/src/backend_api/app/core/schemas/currency.py b/src/backend_api/app/core/schemas/currency.py index 95c6b744..0f09d102 100644 --- a/src/backend_api/app/core/schemas/currency.py +++ b/src/backend_api/app/core/schemas/currency.py @@ -7,6 +7,7 @@ class _BaseCurrency(_pydantic.BaseModel): tradeName: str valueInChaos: float + leagueId: int # Properties to receive on currency creation @@ -33,3 +34,9 @@ class Currency(CurrencyInDBBase): # Properties stored in DB class CurrencyInDB(CurrencyInDBBase): pass + + +class CurrencyQuery(_pydantic.BaseModel): + createdHoursSinceLaunch: int | None = None + tradeName: str | None = None + leagueId: int | None = None diff --git a/src/backend_api/app/core/schemas/item.py b/src/backend_api/app/core/schemas/item.py index 6b0179be..c42c04a3 100644 --- a/src/backend_api/app/core/schemas/item.py +++ b/src/backend_api/app/core/schemas/item.py @@ -15,7 +15,7 @@ class _BaseItem(_pydantic.BaseModel): model_config = _pydantic.ConfigDict(from_attributes=True) name: str | None = None - league: str + leagueId: int itemBaseTypeId: int ilvl: int rarity: str diff --git a/src/backend_api/app/core/schemas/league.py b/src/backend_api/app/core/schemas/league.py new file mode 100644 index 00000000..a9e4f900 --- /dev/null +++ b/src/backend_api/app/core/schemas/league.py @@ -0,0 +1,38 @@ +import datetime as _dt + +import pydantic as _pydantic + + +# Shared League props +class _BaseLeague(_pydantic.BaseModel): + model_config = _pydantic.ConfigDict(from_attributes=True) + + name: str + validFrom: _dt.datetime + validTo: _dt.datetime | None = None + version: float + + +# Properties to receive on League creation +class LeagueCreate(_BaseLeague): + pass + + +# Properties to receive on update +class LeagueUpdate(_BaseLeague): + leagueId: int + + +# Properties shared by models stored in DB +class LeagueInDBBase(_BaseLeague): + leagueId: int + + +# Properties to return to client +class League(LeagueInDBBase): + pass + + +# Properties stored in DB +class LeagueInDB(LeagueInDBBase): + pass diff --git a/src/backend_api/app/core/schemas/plot/input.py b/src/backend_api/app/core/schemas/plot/input.py index a7f333eb..9071528c 100644 --- a/src/backend_api/app/core/schemas/plot/input.py +++ b/src/backend_api/app/core/schemas/plot/input.py @@ -39,7 +39,7 @@ class WantedModifier(_pydantic.BaseModel): class BasePlotQuery(_pydantic.BaseModel): - league: list[str] | str + leagueId: list[int] | int itemSpecifications: ItemSpecs | None = None baseSpecifications: BaseSpecs | None = None end: int | None = None diff --git a/src/backend_api/app/core/schemas/plot/output.py b/src/backend_api/app/core/schemas/plot/output.py index 998f3b19..d1e85c09 100644 --- a/src/backend_api/app/core/schemas/plot/output.py +++ b/src/backend_api/app/core/schemas/plot/output.py @@ -11,7 +11,7 @@ class Datum(_pydantic.BaseModel): class TimeseriesData(_pydantic.BaseModel): - name: str + leagueId: int data: list[Datum] confidenceRating: Literal["low", "medium", "high"] diff --git a/src/backend_api/app/core/schemas/unidentified_item.py b/src/backend_api/app/core/schemas/unidentified_item.py index 342db99c..90214f4d 100644 --- a/src/backend_api/app/core/schemas/unidentified_item.py +++ b/src/backend_api/app/core/schemas/unidentified_item.py @@ -6,13 +6,15 @@ class _BaseUnidentifiedItem(_pydantic.BaseModel): model_config = _pydantic.ConfigDict(from_attributes=True) name: str | None = None - league: str + leagueId: int itemBaseTypeId: int ilvl: int rarity: str identified: bool = False currencyAmount: float | None = None currencyId: int | None = None + nItems: int = 1 + aggregated: bool = False # Properties to receive on item creation @@ -29,8 +31,6 @@ class UnidentifiedItemUpdate(_BaseUnidentifiedItem): class UnidentifiedItemInDBBase(_BaseUnidentifiedItem): createdHoursSinceLaunch: int itemId: int - nItems: int - aggregated: bool # Properties to return to client diff --git a/src/backend_api/app/crud/__init__.py b/src/backend_api/app/crud/__init__.py index 62977e8e..e0b656aa 100644 --- a/src/backend_api/app/crud/__init__.py +++ b/src/backend_api/app/crud/__init__.py @@ -2,9 +2,10 @@ from app.core.models.models import Item as model_Item from app.core.models.models import ItemBaseType as model_ItemBaseType from app.core.models.models import ItemModifier as model_ItemModifier +from app.core.models.models import League as model_League from app.core.models.models import Modifier as model_Modifier from app.core.models.models import UnidentifiedItem as model_UnidentifiedItem -from app.core.schemas.currency import Currency, CurrencyCreate, CurrencyUpdate +from app.core.schemas.currency import Currency, CurrencyCreate from app.core.schemas.item import Item, ItemCreate, ItemUpdate from app.core.schemas.item_base_type import ( ItemBaseType, @@ -16,23 +17,26 @@ ItemModifierCreate, ItemModifierUpdate, ) +from app.core.schemas.league import ( + League, + LeagueCreate, +) from app.core.schemas.modifier import Modifier, ModifierCreate from app.core.schemas.unidentified_item import ( UnidentifiedItem, UnidentifiedItemCreate, - UnidentifiedItemUpdate, ) +from app.crud.extensions.crud_currency import CRUDCurrency +from app.crud.extensions.crud_league import CRUDLeague from app.crud.extensions.crud_modifier import CRUDModifier +from app.crud.extensions.crud_unidentifiedItem import CRUDUnidentifiedItem from app.crud.user import CRUDUser from .base import CRUDBase -CRUD_currency = CRUDBase[ - model_Currency, - Currency, - CurrencyCreate, - CurrencyUpdate, -]( +CRUD_league = CRUDLeague(model=model_League, schema=League, create_schema=LeagueCreate) + +CRUD_currency = CRUDCurrency( model=model_Currency, schema=Currency, create_schema=CurrencyCreate, @@ -62,12 +66,7 @@ ItemUpdate, ](model=model_Item, schema=Item, create_schema=ItemCreate) -CRUD_unidentifiedItem = CRUDBase[ - model_UnidentifiedItem, - UnidentifiedItem, - UnidentifiedItemCreate, - UnidentifiedItemUpdate, -]( +CRUD_unidentifiedItem = CRUDUnidentifiedItem( model=model_UnidentifiedItem, schema=UnidentifiedItem, create_schema=UnidentifiedItemCreate, diff --git a/src/backend_api/app/crud/extensions/crud_currency.py b/src/backend_api/app/crud/extensions/crud_currency.py new file mode 100644 index 00000000..3a094bb9 --- /dev/null +++ b/src/backend_api/app/crud/extensions/crud_currency.py @@ -0,0 +1,103 @@ +from pydantic import TypeAdapter +from sqlalchemy import select +from sqlalchemy.orm import Session +from sqlalchemy.sql.expression import and_, func, or_ + +from app.core.models.models import Currency as model_Currency +from app.core.schemas.currency import ( + Currency, + CurrencyCreate, + CurrencyQuery, + CurrencyUpdate, +) +from app.crud.base import CRUDBase + + +class CRUDCurrency( + CRUDBase[ + model_Currency, + Currency, + CurrencyCreate, + CurrencyUpdate, + ] +): + async def get_latest_currency_id(self, db: Session) -> int: + stmt = select( + func.max(model_Currency.currencyId).label("latestCurrencyId") + ).limit(1) + + db_latest_currency_id = db.execute(stmt).mappings().first() + if db_latest_currency_id is not None: + latest_currency_id = db_latest_currency_id["latestCurrencyId"] + else: + latest_currency_id = 1 + + validate = TypeAdapter(int).validate_python + + return validate(latest_currency_id) + + def _latest_hours_stmt(self, league_ids: list[int]): + return ( + select( + model_Currency.leagueId, + func.max(model_Currency.createdHoursSinceLaunch).label("latest_hour"), + ) + .where(model_Currency.leagueId.in_(league_ids)) + .group_by(model_Currency.leagueId) + ) + + async def get_latest_hours( + self, db: Session, league_ids: list[int] + ) -> dict[int, int]: + stmt = self._latest_hours_stmt(league_ids) + + objs = db.execute(stmt).mappings().all() + id_hour_map = {obj["leagueId"]: obj["latest_hour"] for obj in objs} + + validate = TypeAdapter(dict[int, int]).validate_python + + return validate(id_hour_map) + + async def get_latest_currencies( + self, db: Session, league_ids: list[int] + ) -> list[Currency]: + latest_hours = self._latest_hours_stmt(league_ids).subquery() + + stmt = select(model_Currency).join( + latest_hours, + (model_Currency.leagueId == latest_hours.c.leagueId) + & (model_Currency.createdHoursSinceLaunch == latest_hours.c.latest_hour), + ) + currencies = db.scalars(stmt).all() + + validate = TypeAdapter(list[Currency]).validate_python + + return validate(currencies) + + async def get_currency_from_query( + self, db: Session, query_list: list[CurrencyQuery] + ) -> list[Currency]: + filters = [] + for query in query_list: + sub_filter = [] + if query.createdHoursSinceLaunch is not None: + sub_filter.append( + model_Currency.createdHoursSinceLaunch + == query.createdHoursSinceLaunch + ) + + if query.tradeName is not None: + sub_filter.append(model_Currency.tradeName == query.tradeName) + + if query.leagueId is not None: + sub_filter.append(model_Currency.leagueId == query.leagueId) + + if sub_filter: + filters.append(and_(*sub_filter)) + stmt = select(model_Currency).where(or_(*filters)) + + currencies = db.execute(stmt).scalars().all() + + validate = TypeAdapter(list[Currency]).validate_python + + return validate(currencies) diff --git a/src/backend_api/app/crud/extensions/crud_league.py b/src/backend_api/app/crud/extensions/crud_league.py new file mode 100644 index 00000000..01af9728 --- /dev/null +++ b/src/backend_api/app/crud/extensions/crud_league.py @@ -0,0 +1,29 @@ +from datetime import datetime + +from pydantic import TypeAdapter +from sqlalchemy import select +from sqlalchemy.orm import Session + +from app.core.models.models import League as model_League +from app.core.schemas.league import ( + League, + LeagueCreate, + LeagueUpdate, +) +from app.crud.base import CRUDBase + + +class CRUDLeague(CRUDBase[model_League, League, LeagueCreate, LeagueUpdate]): + async def get_active_leagues(self, db: Session) -> list[League]: + now = datetime.now() + + stmt = select(model_League).where( + model_League.validFrom <= now, + model_League.validTo.is_(None) | (model_League.validTo >= now), + ) + + leagues = db.execute(stmt).scalars().all() + + validate = TypeAdapter(list[League]).validate_python + + return validate(leagues) diff --git a/src/backend_api/app/crud/extensions/crud_modifier.py b/src/backend_api/app/crud/extensions/crud_modifier.py index 1c8f0652..9d291ea4 100644 --- a/src/backend_api/app/crud/extensions/crud_modifier.py +++ b/src/backend_api/app/crud/extensions/crud_modifier.py @@ -21,7 +21,9 @@ class CRUDModifier( ModifierUpdate, ] ): - async def get_grouped_modifier_by_effect(self, db: Session): + async def get_grouped_modifier_by_effect( + self, db: Session + ) -> GroupedModifierByEffect: stmt = select( model_Modifier.modifierId, func.min(model_Modifier.effect).label("effect"), diff --git a/src/backend_api/app/crud/extensions/crud_unidentifiedItem.py b/src/backend_api/app/crud/extensions/crud_unidentifiedItem.py new file mode 100644 index 00000000..3e3ad12e --- /dev/null +++ b/src/backend_api/app/crud/extensions/crud_unidentifiedItem.py @@ -0,0 +1,44 @@ +from pydantic import TypeAdapter +from sqlalchemy import delete, select +from sqlalchemy.orm import Session + +from app.core.models.models import UnidentifiedItem as model_UnidentifiedItem +from app.core.schemas.unidentified_item import ( + UnidentifiedItem, + UnidentifiedItemCreate, + UnidentifiedItemUpdate, +) +from app.crud.base import CRUDBase + + +class CRUDUnidentifiedItem( + CRUDBase[ + model_UnidentifiedItem, + UnidentifiedItem, + UnidentifiedItemCreate, + UnidentifiedItemUpdate, + ] +): + async def get_non_aggregated(self, db: Session) -> list[UnidentifiedItem]: + """ + Returns the non aggregated unidentified items + """ + stmt = select(model_UnidentifiedItem).where( + model_UnidentifiedItem.aggregated.isnot(True), + ) + + non_aggregated = db.execute(stmt).scalars().all() + + validate = TypeAdapter(list[UnidentifiedItem]).validate_python + + return validate(non_aggregated) + + async def add_aggregated( + self, db: Session, aggregated_objs: list[UnidentifiedItemCreate] + ): + stmt = delete(model_UnidentifiedItem).where( + model_UnidentifiedItem.aggregated.isnot(True), + ) + db.execute(stmt) + + await self.create(db, obj_in=aggregated_objs, return_nothing=True) diff --git a/src/backend_api/app/plotting/plotter.py b/src/backend_api/app/plotting/plotter.py index bbdb033e..013a88db 100644 --- a/src/backend_api/app/plotting/plotter.py +++ b/src/backend_api/app/plotting/plotter.py @@ -97,7 +97,7 @@ def _init_stmt( select_args: list[InstrumentedAttribute[Any] | Label[Any]] = [ item_model.itemId, item_model.createdHoursSinceLaunch, - item_model.league, + item_model.leagueId, item_model.itemBaseTypeId, item_model.currencyId, item_model.currencyAmount, @@ -114,12 +114,12 @@ def _init_stmt( model_Currency, item_model.currencyId == model_Currency.currencyId ) - if isinstance(query.league, list): + if isinstance(query.leagueId, list): stmt = stmt.where( - or_(*[item_model.league == league for league in query.league]) + or_(*[item_model.leagueId == league for league in query.leagueId]) ) else: - stmt = stmt.where(model_Item.league == query.league) + stmt = stmt.where(model_Item.leagueId == query.leagueId) if start is not None: stmt = stmt.where(item_model.createdHoursSinceLaunch >= start) @@ -223,8 +223,8 @@ def _create_plot_data(self, df: pd.DataFrame) -> PlotData: 0, "divine" ) # TODO: set enum data = [] - for league in df["league"].unique(): - league_df: pd.DataFrame = df.loc[df["league"] == league] + for league in df["leagueId"].unique(): + league_df: pd.DataFrame = df.loc[df["leagueId"] == league] league_data = league_df[ [ "hoursSinceLaunch", @@ -234,7 +234,7 @@ def _create_plot_data(self, df: pd.DataFrame) -> PlotData: ] ].to_dict(orient="records") # type: ignore timeseries_data = { - "name": league, + "leagueId": league, "confidenceRating": league_df["confidence"].mode()[0], "data": league_data, } @@ -443,7 +443,7 @@ def _create_plot_statement(self, query: IdentifiedPlotQuery) -> Select: base_query.c.tradeName.label("mostCommonTradeName"), func.count(base_query.c.tradeName).label("nameCount"), ) - .group_by(base_query.c.league, base_query.c.tradeName) + .group_by(base_query.c.leagueId, base_query.c.tradeName) .order_by(desc("nameCount")) # Can use literal_column in complex cases .limit(1) .cte("mostCommon") @@ -486,7 +486,7 @@ def _create_plot_statement(self, query: IdentifiedPlotQuery) -> Select: prices = ( select( base_query.c.createdHoursSinceLaunch, - base_query.c.league, + base_query.c.leagueId, (base_query.c.currencyAmount * base_query.c.valueInChaos).label( "valueInChaos" ), @@ -519,7 +519,7 @@ def _create_plot_statement(self, query: IdentifiedPlotQuery) -> Select: filtered_prices = ( select( ranked_prices.c.createdHoursSinceLaunch, - ranked_prices.c.league, + ranked_prices.c.leagueId, ranked_prices.c.valueInChaos, ranked_prices.c.valueInMostCommonCurrencyUsed, ranked_prices.c.mostCommonCurrencyUsed, @@ -537,7 +537,7 @@ def _create_plot_statement(self, query: IdentifiedPlotQuery) -> Select: final_query = ( select( filtered_prices.c.createdHoursSinceLaunch.label("hoursSinceLaunch"), - filtered_prices.c.league, + filtered_prices.c.leagueId, func.avg(filtered_prices.c.valueInChaos).label("valueInChaos"), func.avg(filtered_prices.c.valueInMostCommonCurrencyUsed).label( "valueInMostCommonCurrencyUsed" @@ -548,7 +548,7 @@ def _create_plot_statement(self, query: IdentifiedPlotQuery) -> Select: func.min(filtered_prices.c.confidence).label("confidence"), ) .group_by( - filtered_prices.c.createdHoursSinceLaunch, filtered_prices.c.league + filtered_prices.c.createdHoursSinceLaunch, filtered_prices.c.leagueId ) .order_by(filtered_prices.c.createdHoursSinceLaunch) ) diff --git a/src/backend_api/app/tests/README.md b/src/backend_api/app/tests/README.md index 52d7edb3..e11ba50b 100644 --- a/src/backend_api/app/tests/README.md +++ b/src/backend_api/app/tests/README.md @@ -14,7 +14,7 @@ The purpose of `test_simulating_env` is to check if base case usage of all the r We test base cases and some outliers for all the crud and route models. -The routes for `currency`, `item_base_type`, `item_modifier`, `item` and `modifier` all inherit from the same generalized crud `base` class. These uses the same test files and classes with specified fixtures for each route and crud. +The routes for `item_base_type`, `item_modifier`, `item` and `modifier` all inherit from the same generalized crud `base` class. These uses the same test files and classes with specified fixtures for each route and crud. There are also specialized tests for routes that doesn't use the crud `base` class. These are `login`, `user`, `plot` and `turnstile`. It doesn't make sence to inherit a base test class with these routes. diff --git a/src/backend_api/app/tests/test_simulating_env/api/routes/test_currency.py b/src/backend_api/app/tests/test_simulating_env/api/routes/test_currency.py index 6a74b4d2..5ed1a9df 100644 --- a/src/backend_api/app/tests/test_simulating_env/api/routes/test_currency.py +++ b/src/backend_api/app/tests/test_simulating_env/api/routes/test_currency.py @@ -7,10 +7,10 @@ from httpx import AsyncClient import app.tests.test_simulating_env.api.api_routes_test_base as test_api -from app.api.routes import currency_prefix +from app.api.routes import currency_prefix, league_prefix from app.api.routes.currency import get_currency from app.core.config import settings -from app.core.models.models import Currency +from app.core.models.models import Currency, League from app.crud import CRUD_currency from app.crud.base import CRUDBase, ModelType from app.tests.utils.model_utils.currency import ( @@ -91,6 +91,45 @@ def object_generator_func() -> Callable[[], tuple[dict, ModelType]]: return generate_random_currency +@pytest.fixture(scope="module") +def object_generator_func_w_deps() -> ( + Callable[[], tuple[dict, Currency, list[dict | League]]] +): + def generate_random_item_w_deps( + db, + ) -> Callable[ + [], + tuple[ + dict, + Currency, + list[dict | League | Currency], + ], + ]: + return generate_random_currency(db, retrieve_dependencies=True) + + return generate_random_item_w_deps + + +@pytest.fixture(scope="module") +def api_deps_instances() -> list[list[str]]: + """Fixture for API dependencies instances. + + Dependencies in return list needs to be in correct order. + If a dependency is dependent on another, the dependency needs to occur later than + the one its dependent on. The order is defined by 'generate_random_item'. + + Returns: + List[list[str]]: API dependencies instances. Format: [dep_route_prefix: dep_unique_identifier] + """ + return [ + [ + league_prefix, + get_model_unique_identifier(League), + League.__tablename__, + ] + ] + + @pytest.fixture(scope="module") def get_high_permissions() -> bool: """Some models require high permissions to test GET requests diff --git a/src/backend_api/app/tests/test_simulating_env/api/routes/test_league.py b/src/backend_api/app/tests/test_simulating_env/api/routes/test_league.py new file mode 100644 index 00000000..249220af --- /dev/null +++ b/src/backend_api/app/tests/test_simulating_env/api/routes/test_league.py @@ -0,0 +1,128 @@ +from collections.abc import Awaitable, Callable +from typing import Any + +import pytest +import pytest_asyncio +from fastapi import Response +from httpx import AsyncClient +from sqlalchemy.orm import Session + +import app.tests.test_simulating_env.api.api_routes_test_base as test_api +from app.api.routes import item_prefix +from app.core.config import settings +from app.core.models.models import League +from app.crud import CRUD_item +from app.crud.base import CRUDBase, ModelType +from app.tests.utils.model_utils.item import ( + create_random_item_dict, + generate_random_item, +) +from app.tests.utils.utils import get_model_table_name, get_model_unique_identifier + + +@pytest.fixture(scope="module") +def route_prefix() -> str: + return item_prefix + + +@pytest.fixture(scope="module") +def is_hypertable() -> bool: + return True + + +@pytest.fixture(scope="module") +def crud_instance() -> CRUDBase: + return CRUD_item + + +@pytest.fixture(scope="module") +def on_duplicate_params() -> tuple[bool, str | None]: + """ + In tuple: + First item: `on_duplicate_do_nothing`. + Second item: `on_duplicate_constraint` (unique constraint to check the duplicate on) + """ + return (False, None) + + +@pytest.fixture(scope="module") +def model_table_name() -> str: + return get_model_table_name(League) + + +@pytest.fixture(scope="module") +def update_request_params() -> bool: + """Some models require params to PUT request in the url + + Returns: + bool: True if the model requires params in the PUT request + """ + return False + + +@pytest.fixture(scope="module") +def ignore_test_columns() -> list[str]: + """Ignore these columns when testing the model + + createdAt are ignored because currently, the API returns + time in a different format than the one stored in the database + + Returns: + List[str]: List of columns to ignore + """ + return [] + + +@pytest.fixture(scope="module") +def unique_identifier() -> str: + unique_identifier = get_model_unique_identifier(League) + return unique_identifier + + +@pytest.fixture(scope="module") +def get_high_permissions() -> bool: + """Some models require high permissions to test GET requests + + Returns: + bool: True if the model requires high permissions to test GET requests + """ + return False + + +@pytest.fixture(scope="module") +def object_generator_func() -> Callable[[], tuple[dict, ModelType]]: + return generate_random_item + + +@pytest.fixture(scope="module") +def create_random_object_func() -> Callable[[Session], Awaitable[dict]]: + async def create_object(db: Session) -> dict: + return await create_random_item_dict(db) + + return create_object + + +@pytest_asyncio.fixture +async def get_object_from_api_normal_user( + async_client: AsyncClient, + route_prefix: str, + unique_identifier: str, + normal_user_token_headers: dict[str, str], +) -> Callable[[Any, Any], Awaitable[Any]]: + async def _get_object(object_pk_map: dict[str, Any]) -> Response: + response = await async_client.get( + f"{settings.API_V1_STR}/{route_prefix}/{object_pk_map[unique_identifier]}", + headers=normal_user_token_headers, + ) + return response + + return _get_object + + +@pytest.fixture(scope="module") +def update_request_params_deps() -> list[str]: + return [] + + +class TestLeague(test_api.TestAPI): + pass diff --git a/src/backend_api/app/tests/test_simulating_env/crud/crud_models/test_league.py b/src/backend_api/app/tests/test_simulating_env/crud/crud_models/test_league.py new file mode 100644 index 00000000..e5dccb9c --- /dev/null +++ b/src/backend_api/app/tests/test_simulating_env/crud/crud_models/test_league.py @@ -0,0 +1,52 @@ +from collections.abc import Callable, Generator + +import pytest +from sqlalchemy.orm import Session + +import app.tests.test_simulating_env.crud.crud_test_base as test_crud +from app.core.models.database import engine +from app.crud import CRUD_league +from app.crud.base import CRUDBase +from app.tests.utils.model_utils.league import generate_random_league + + +@pytest.fixture(scope="session") +def db() -> Generator: + with Session(engine) as session: + yield session + session.rollback() + session.close() + + +@pytest.fixture(scope="module") +def is_hypertable() -> bool: + return False + + +@pytest.fixture(scope="module") +def on_duplicate_params() -> tuple[bool, str | None]: + """ + In tuple: + First item: `on_duplicate_do_nothing`. + Second item: `on_duplicate_constraint` (unique constraint to check the duplicate on) + """ + return (False, None) + + +@pytest.fixture(scope="module") +def object_generator_func() -> Callable[[], dict]: + return generate_random_league + + +@pytest.fixture(scope="module") +def main_key() -> str: + return None + + +@pytest.fixture(scope="module") +def crud_instance() -> CRUDBase: + return CRUD_league + + +class TestLeagueCRUD(test_crud.TestCRUD): + pass diff --git a/src/backend_api/app/tests/utils/model_utils/currency.py b/src/backend_api/app/tests/utils/model_utils/currency.py index 7fdc6bee..7b733aba 100644 --- a/src/backend_api/app/tests/utils/model_utils/currency.py +++ b/src/backend_api/app/tests/utils/model_utils/currency.py @@ -1,8 +1,9 @@ from sqlalchemy.orm import Session from app import crud -from app.core.models.models import Currency +from app.core.models.models import Currency, League from app.core.schemas import CurrencyCreate +from app.tests.utils.model_utils.league import generate_random_league from app.tests.utils.utils import ( random_float, random_int, @@ -10,35 +11,61 @@ ) -def create_random_currency_dict() -> dict: +async def create_random_currency_dict( + db: Session, retrieve_dependencies: bool | None = False +) -> dict | tuple[dict, list[dict | League]]: """Create a random currency dictionary. + Args: + db (Session): DB session. + retrieve_dependencies (bool, optional): Whether to retrieve dependencies. Defaults to False. + Returns: - Dict: Currency dictionary with random values. + Union[ Dict, Tuple[ Dict, List[ Union[ Dict, League] ], ], ]: \n + Random Currency dictionary or tuple with random Currency dictionary and dependencies. """ tradeName = random_lower_string() valueInChaos = random_float() createdHoursSinceLaunch = random_int(small_int=True) + league_dict, league = await generate_random_league(db) + leagueId = league.leagueId + currency = { "tradeName": tradeName, "valueInChaos": valueInChaos, "createdHoursSinceLaunch": createdHoursSinceLaunch, + "leagueId": leagueId, } - - return currency + if not retrieve_dependencies: + return currency + else: + deps = [] + deps += [league_dict, league] + return currency, deps -async def generate_random_currency(db: Session) -> tuple[dict, Currency]: +async def generate_random_currency( + db: Session, retrieve_dependencies: bool | None = False +) -> tuple[dict, Currency, list[dict | League] | None]: """Generates a random currency. Args: db (Session): DB session. Returns: - Tuple[Dict, Currency]: Random currency dict and Currency db object. + Tuple[ Dict, Currency, List[ Union[ Dict, League, ] ] ], ]: \n + Random currency dict and Currency db object and optional dependencies. """ - currency_dict = create_random_currency_dict() + output = await create_random_currency_dict(db, retrieve_dependencies) + if not retrieve_dependencies: + currency_dict = output + else: + currency_dict, deps = output currency_create = CurrencyCreate(**currency_dict) currency = await crud.CRUD_currency.create(db, obj_in=currency_create) - return currency_dict, currency + + if not retrieve_dependencies: + return currency_dict, currency + else: + return currency_dict, currency, deps diff --git a/src/backend_api/app/tests/utils/model_utils/item.py b/src/backend_api/app/tests/utils/model_utils/item.py index da80ba6a..c9dde2dd 100644 --- a/src/backend_api/app/tests/utils/model_utils/item.py +++ b/src/backend_api/app/tests/utils/model_utils/item.py @@ -1,10 +1,11 @@ from sqlalchemy.orm import Session from app import crud -from app.core.models.models import Currency, Item, ItemBaseType +from app.core.models.models import Currency, Item, ItemBaseType, League from app.core.schemas.item import ItemCreate from app.tests.utils.model_utils.currency import generate_random_currency from app.tests.utils.model_utils.item_base_type import generate_random_item_base_type +from app.tests.utils.model_utils.league import generate_random_league from app.tests.utils.utils import ( random_bool, random_float, @@ -16,7 +17,7 @@ async def create_random_item_dict( db: Session, retrieve_dependencies: bool | None = False -) -> dict | tuple[dict, list[dict | ItemBaseType | Currency]]: +) -> dict | tuple[dict, list[dict | ItemBaseType | Currency | League]]: """Create a random item dictionary. Args: @@ -24,11 +25,10 @@ async def create_random_item_dict( retrieve_dependencies (bool, optional): Whether to retrieve dependencies. Defaults to False. Returns: - Union[ Dict, Tuple[ Dict, List[ Union[ Dict, ItemBaseType, Currency, ] ], ], ]: \n + Union[ Dict, Tuple[ Dict, List[ Union[ Dict, ItemBaseType, Currency, League] ], ], ]: \n Random item dictionary or tuple with random item dictionary and dependencies. """ name = random_lower_string() - league = random_lower_string() ilvl = random_int(small_int=True) rarity = random_lower_string() identified = random_bool() @@ -58,10 +58,12 @@ async def create_random_item_dict( itemBaseTypeId = item_base_type.itemBaseTypeId currency_dict, currency = await generate_random_currency(db) currencyId = currency.currencyId + league_dict, league = await generate_random_league(db) + leagueId = league.leagueId item = { "name": name, - "league": league, + "leagueId": leagueId, "itemBaseTypeId": itemBaseTypeId, "createdHoursSinceLaunch": createdHoursSinceLaunch, "ilvl": ilvl, @@ -88,6 +90,7 @@ async def create_random_item_dict( deps = [] deps += [item_base_type_dict, item_base_type] deps += [currency_dict, currency] + deps += [league_dict, league] return item, deps @@ -96,7 +99,7 @@ async def generate_random_item( ) -> tuple[ dict, Item, - list[dict | ItemBaseType | Currency] | None, + list[dict | ItemBaseType | Currency | League] | None, ]: """Generate a random item. diff --git a/src/backend_api/app/tests/utils/model_utils/item_modifier.py b/src/backend_api/app/tests/utils/model_utils/item_modifier.py index b287d9e1..d28735bf 100644 --- a/src/backend_api/app/tests/utils/model_utils/item_modifier.py +++ b/src/backend_api/app/tests/utils/model_utils/item_modifier.py @@ -6,6 +6,7 @@ Item, ItemBaseType, ItemModifier, + League, Modifier, ) from app.core.schemas.item_modifier import ItemModifierCreate @@ -20,7 +21,7 @@ async def create_random_item_modifier_dict( dict | tuple[ dict, - list[dict | ItemBaseType | Currency | Item | Modifier] | None, + list[dict | ItemBaseType | Currency | League | Item | Modifier] | None, ] ): """Create a random item modifier dictionary. @@ -66,7 +67,7 @@ async def generate_random_item_modifier( ) -> tuple[ dict, ItemModifier, - list[dict | ItemBaseType | Currency | Item | Modifier] | None, + list[dict | ItemBaseType | Currency | League | Item | Modifier] | None, ]: """Generate a random item modifier. diff --git a/src/backend_api/app/tests/utils/model_utils/league.py b/src/backend_api/app/tests/utils/model_utils/league.py new file mode 100644 index 00000000..aa2fc308 --- /dev/null +++ b/src/backend_api/app/tests/utils/model_utils/league.py @@ -0,0 +1,46 @@ +from sqlalchemy.orm import Session + +from app import crud +from app.core.models.models import League +from app.core.schemas import LeagueCreate +from app.tests.utils.utils import ( + random_datetime, + random_float, + random_lower_string, +) + + +def create_random_league_dict() -> dict: + """Create a random league dictionary. + + Returns: + Dict: League dictionary with random values. + """ + name = random_lower_string() + validFrom = random_datetime() + validTo = random_datetime(min_time=validFrom) + version = random_float(small_float=True, max_value=3.99) + + league = { + "name": name, + "validFrom": validFrom, + "validTo": validTo, + "version": version, + } + + return league + + +async def generate_random_league(db: Session) -> tuple[dict, League]: + """Generates a random league. + + Args: + db (Session): DB session. + + Returns: + Tuple[Dict, League]: Random league dict and League db object. + """ + league_dict = create_random_league_dict() + league_create = LeagueCreate(**league_dict) + league = await crud.CRUD_league.create(db, obj_in=league_create) + return league_dict, league diff --git a/src/backend_api/app/tests/utils/model_utils/plot.py b/src/backend_api/app/tests/utils/model_utils/plot.py index eae196dd..f71e91e6 100644 --- a/src/backend_api/app/tests/utils/model_utils/plot.py +++ b/src/backend_api/app/tests/utils/model_utils/plot.py @@ -3,7 +3,7 @@ from sqlalchemy.orm import Session from app.core.models.models import ( - Item as model_Item, + League as model_League, ) from app.core.models.models import ( Modifier as model_Modifier, @@ -24,26 +24,9 @@ async def create_minimal_random_plot_query_dict(db: Session) -> dict[str, Any]: db, retrieve_dependencies=True ) - item_dep: model_Item = item_modifier_deps[-3] + league_dep: model_League = item_modifier_deps[-3] modifier_dep: model_Modifier = item_modifier_deps[-1] - item_specs = { - "name": None, - "identified": None, - "minIlvl": None, - "maxIlvl": None, - "rarity": None, - "corrupted": None, - "delve": None, - "fractured": None, - "synthesised": None, - "replica": None, - "influences": None, - "searing": None, - "tangled": None, - "foilVariation": None, - } - wanted_modifiers = [ { "modifierId": modifier_dep.modifierId, @@ -51,8 +34,7 @@ async def create_minimal_random_plot_query_dict(db: Session) -> dict[str, Any]: ] plot_query = { - "league": item_dep.league, - "itemSpecifications": item_specs, + "leagueId": league_dep.leagueId, "wantedModifiers": wanted_modifiers, } diff --git a/src/backend_api/app/tests/utils/utils.py b/src/backend_api/app/tests/utils/utils.py index 0dfa5c44..405a6ed6 100644 --- a/src/backend_api/app/tests/utils/utils.py +++ b/src/backend_api/app/tests/utils/utils.py @@ -1,7 +1,7 @@ import random import string from collections.abc import Callable -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from inspect import iscoroutinefunction from typing import Any @@ -130,13 +130,22 @@ def random_url() -> str: return f"https://{random_lower_string()}.{random_lower_string(small_string=True)}" -def random_datetime() -> datetime: +def random_datetime(min_time: datetime = None, max_time: datetime = None) -> datetime: """Generate a random datetime. Returns: datetime: Random datetime. """ - return datetime.now() + random.uniform(-5, 5) * timedelta(days=1) + time = datetime.now(tz=timezone.utc) + bottom = -5 + top = 5 + if min_time is not None: + time = min(time, min_time) + bottom = 0 + if max_time is not None: + time = max(time, max_time) + top = 0 + return time + random.uniform(bottom, top) * timedelta(days=1) def random_based_on_type(reference: str | float | int) -> str | int | float: diff --git a/src/backend_data_retrieval/data_retrieval_app/data_deposit/item_base_type/item_base_type_data_depositor.py b/src/backend_data_retrieval/data_retrieval_app/data_deposit/item_base_type/item_base_type_data_depositor.py index bad7d88b..8d871faa 100644 --- a/src/backend_data_retrieval/data_retrieval_app/data_deposit/item_base_type/item_base_type_data_depositor.py +++ b/src/backend_data_retrieval/data_retrieval_app/data_deposit/item_base_type/item_base_type_data_depositor.py @@ -5,6 +5,7 @@ from data_retrieval_app.data_deposit.data_depositor_base import DataDepositorBase from data_retrieval_app.logs.logger import data_deposit_logger as logger +from data_retrieval_app.utils import get_data_safe class ItemBaseTypeDataDepositor(DataDepositorBase): @@ -18,14 +19,12 @@ def __init__(self) -> None: def _get_current_base_types(self) -> pd.DataFrame: logger.info("Retrieving previously deposited data.") - response = requests.get(self.data_url, headers=self.pom_auth_headers) + response = get_data_safe( + self.data_url, headers=self.pom_auth_headers, logger=logger + ) - df = pd.DataFrame() - # Check if the request was successful - if response.status_code == 200: - # Load the JSON data into a pandas DataFrame - json_io = StringIO(response.content.decode("utf-8")) - df = pd.read_json(json_io, dtype=str) + json_io = StringIO(response.content.decode("utf-8")) + df = pd.read_json(json_io, dtype=str) if df.empty: logger.info("Found no previously deposited data.") @@ -93,7 +92,7 @@ def _update_duplicates(self, duplicate_df: pd.DataFrame): response.raise_for_status() except Exception as e: logger.error( - f"The following error occurred while making request during _update_duplicates modifiers: {e}" + f"The following error occurred while making request during _update_duplicates item base types: {e}" ) raise e diff --git a/src/backend_data_retrieval/data_retrieval_app/data_deposit/league/__init__.py b/src/backend_data_retrieval/data_retrieval_app/data_deposit/league/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/backend_data_retrieval/data_retrieval_app/data_deposit/league/league_data/leagues.csv b/src/backend_data_retrieval/data_retrieval_app/data_deposit/league/league_data/leagues.csv new file mode 100644 index 00000000..339f675a --- /dev/null +++ b/src/backend_data_retrieval/data_retrieval_app/data_deposit/league/league_data/leagues.csv @@ -0,0 +1,9 @@ +# Source: https://www.poewiki.net/wiki/League#Challenge_leagues +name,validFrom,validTo,version +Mirage,2026-03-06T19:00:00Z,2026-07-20T22:00:00Z,3.28 +Phrecia2.0,2026-01-29T19:00:00Z,2026-02-19T21:00:00Z,3.27 +Keepers,2025-10-31T19:00:00Z,2026-03-02T21:00:00Z,3.27 +Mercenaries,2025-06-13T20:00:00Z,2025-10-27T21:00:00Z,3.26 +Phrecia,2025-02-20T20:00:00Z,2025-04-23T21:00:00Z,3.25 +Necro Settlers,2024-11-07T20:00:00Z,2025-02-20T19:00:00Z,3.25 +Settlers,2024-07-26T20:00:00Z,2025-06-09T22:00:00Z,3.25 \ No newline at end of file diff --git a/src/backend_data_retrieval/data_retrieval_app/data_deposit/league/league_data_depositor.py b/src/backend_data_retrieval/data_retrieval_app/data_deposit/league/league_data_depositor.py new file mode 100644 index 00000000..bc87ef22 --- /dev/null +++ b/src/backend_data_retrieval/data_retrieval_app/data_deposit/league/league_data_depositor.py @@ -0,0 +1,99 @@ +from io import StringIO + +import pandas as pd +import requests + +from data_retrieval_app.data_deposit.data_depositor_base import DataDepositorBase +from data_retrieval_app.logs.logger import data_deposit_logger as logger +from data_retrieval_app.utils import get_data_safe + + +class LeagueDataDepositor(DataDepositorBase): + def __init__(self) -> None: + super().__init__(data_type="league") + + self.update_url = str(self.data_url) + self.data_url += "?on_duplicate_pkey_do_nothing=true" + self.current_league_df = self._get_current_leagues() + + def _get_current_leagues(self) -> pd.DataFrame: + logger.info("Retrieving previously deposited data.") + + response = get_data_safe( + self.data_url, headers=self.pom_auth_headers, logger=logger + ) + + json_io = StringIO(response.content.decode("utf-8")) + df = pd.read_json(json_io, dtype=str) + + if df.empty: + logger.info("Found no previously deposited data.") + return pd.DataFrame(columns=["name", "leagueId", "validTo", "validFrom"]) + else: + logger.info("Successfully retrieved previously deposited data.") + return df + + def _update_duplicates(self, duplicate_df: pd.DataFrame): + for _, duplicate_league_row in duplicate_df.iterrows(): + league_id = int(duplicate_league_row["leagueId"]) + + old_df = duplicate_league_row[ + [col for col in duplicate_league_row.index if not col.endswith("_y")] + ] + old_df.index = old_df.index.str.removesuffix("_x") + old = old_df.to_dict() + new_df = duplicate_league_row[ + [col for col in duplicate_league_row.index if not col.endswith("_x")] + ] + new_df.index = new_df.index.str.removesuffix("_y") + new = new_df.to_dict() + + do_update = False + for key, old_val in old.items(): + if new[key] != old_val: + do_update = True + break + + if do_update: + headers = { + "accept": "application/json", + "Content-Type": "application/json", + } + headers.update(self.pom_auth_headers) + try: + response = requests.put( + self.update_url + str(league_id), + json=new, + headers=headers, + # add HTTP Basic Auth + ) + response.raise_for_status() + except Exception as e: + logger.error( + f"The following error occurred while making request during _update_duplicates league: {e}" + ) + raise e + + def _process_data(self, df: pd.DataFrame) -> pd.DataFrame: + hardcore_df = df.copy() + hardcore_df["name"] = "Hardcore " + hardcore_df["name"] + df = pd.concat((df, hardcore_df), ignore_index=True) + merged_df = pd.merge( + df, + self.current_league_df, + how="left", + on="name", + suffixes=("_x", "_y"), + ) + non_duplicate_mask = merged_df["leagueId"].isna() + non_duplicate_df = merged_df[non_duplicate_mask] + non_duplicate_df = non_duplicate_df.drop( + columns=[ + column for column in non_duplicate_df.columns if column.endswith("_y") + ] + ) + non_duplicate_df.columns = non_duplicate_df.columns.str.removesuffix("_x") + + self._update_duplicates(merged_df[~non_duplicate_mask]) + + return non_duplicate_df diff --git a/src/backend_data_retrieval/data_retrieval_app/data_deposit/main.py b/src/backend_data_retrieval/data_retrieval_app/data_deposit/main.py index d5e5490a..342c0bbc 100644 --- a/src/backend_data_retrieval/data_retrieval_app/data_deposit/main.py +++ b/src/backend_data_retrieval/data_retrieval_app/data_deposit/main.py @@ -1,9 +1,10 @@ -from typing import Literal - from data_retrieval_app.data_deposit.data_depositor_base import DataDepositorBase from data_retrieval_app.data_deposit.item_base_type.item_base_type_data_depositor import ( ItemBaseTypeDataDepositor, ) +from data_retrieval_app.data_deposit.league.league_data_depositor import ( + LeagueDataDepositor, +) from data_retrieval_app.data_deposit.modifier.modifier_data_depositor import ( ModifierDataDepositor, ) @@ -14,9 +15,10 @@ def main(): setup_logging() logger.info("Starting deposit phase.") - data_depositors: dict[Literal["modifier", "itemBaseType"], DataDepositorBase] = { + data_depositors: dict[str, DataDepositorBase] = { "modifer": ModifierDataDepositor(), "itemBaseType": ItemBaseTypeDataDepositor(), + "league": LeagueDataDepositor(), } for key, data_depositor in data_depositors.items(): logger.info(f"Depositing {key} data.") diff --git a/src/backend_data_retrieval/data_retrieval_app/data_deposit/modifier/modifier_data_depositor.py b/src/backend_data_retrieval/data_retrieval_app/data_deposit/modifier/modifier_data_depositor.py index 7d16ee9f..d9c606af 100644 --- a/src/backend_data_retrieval/data_retrieval_app/data_deposit/modifier/modifier_data_depositor.py +++ b/src/backend_data_retrieval/data_retrieval_app/data_deposit/modifier/modifier_data_depositor.py @@ -13,7 +13,7 @@ do_update_regex, ) from data_retrieval_app.logs.logger import data_deposit_logger as logger -from data_retrieval_app.utils import df_to_JSON +from data_retrieval_app.utils import df_to_JSON, get_data_safe CASCADING_UPDATE = True @@ -40,14 +40,12 @@ def __init__(self) -> None: def _get_current_modifiers(self) -> pd.DataFrame: logger.info("Retrieving previously deposited data.") - response = requests.get(self.data_url, headers=self.pom_auth_headers) + response = get_data_safe( + self.data_url, headers=self.pom_auth_headers, logger=logger + ) - df = pd.DataFrame() - # Check if the request was successful - if response.status_code == 200: - # Load the JSON data into a pandas DataFrame - json_io = StringIO(response.content.decode("utf-8")) - df = pd.read_json(json_io, dtype=str) + json_io = StringIO(response.content.decode("utf-8")) + df = pd.read_json(json_io, dtype=str) if df.empty: logger.info("Found no previously deposited data.") diff --git a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/config.py b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/config.py index edeed8ab..59796e8e 100644 --- a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/config.py +++ b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/config.py @@ -1,5 +1,3 @@ -from datetime import datetime - from pydantic import ( HttpUrl, computed_field, @@ -27,12 +25,6 @@ def BACKEND_BASE_URL(self) -> HttpUrl: OATH_ACC_TOKEN_CONTACT_EMAIL: str FIRST_SUPERUSER: str FIRST_SUPERUSER_PASSWORD: str - CURRENT_SOFTCORE_LEAGUE: str - - @computed_field # type: ignore[prop-decorator] - @property - def CURRENT_HARDCORE_LEAGUE(self) -> str: - return f"Hardcore {self.CURRENT_SOFTCORE_LEAGUE}" POE_PUBLIC_STASHES_AUTH_TOKEN: str OAUTH_CLIENT_ID: str @@ -41,15 +33,7 @@ def CURRENT_HARDCORE_LEAGUE(self) -> str: MINI_BATCH_SIZE: int = 30 N_CHECKPOINTS_PER_TRANSFORMATION: int = 10 - TIME_BETWEEN_RESTART: int = 3600 MAX_TIME_PER_MINI_BATCH: int = 3 * 60 - LEAGUE_LAUNCH_TIME: str - - @computed_field # type: ignore[prop-decorator] - @property - def LEAGUE_LAUNCH_DATETIME_OBJECT(self) -> datetime: - return datetime.fromisoformat(self.LEAGUE_LAUNCH_TIME) - settings = Settings() # type: ignore diff --git a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/data_retrieval/currency_api_handler.py b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/data_retrieval/currency_api_handler.py index c78fbb70..a5c1b6e4 100644 --- a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/data_retrieval/currency_api_handler.py +++ b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/data_retrieval/currency_api_handler.py @@ -1,7 +1,9 @@ +from typing import Any + import pandas as pd -import requests from data_retrieval_app.logs.logger import external_data_retrieval_logger as logger +from data_retrieval_app.utils import get_data_safe class CurrencyAPIHandler: @@ -13,25 +15,25 @@ def _json_to_df(self, currencies: list) -> pd.DataFrame: return df - def make_request(self) -> pd.DataFrame: + def make_request(self, leagues: list[dict[str, Any]]) -> pd.DataFrame: """ Makes an initial, synchronous, API call. """ - try: - response = requests.get(self.url) - response.raise_for_status() - except Exception as e: - logger.error( - f"The following error occurred while making request in poe ninja currency handler: {e}" + df = None + for league in leagues: + response = get_data_safe( + self.url.format(league=league["name"].replace(" ", "+")), logger=logger ) - raise e - response_json = response.json() - - # items_df = pd.DataFrame(response_json["items"]) - items_df = pd.json_normalize(response_json["items"]) - currency_df = items_df[items_df["category"] == "currency"] - - return currency_df + response_json = response.json() + + items_df = pd.json_normalize(response_json["items"]) + currency_df = items_df[items_df["category"] == "currency"] + currency_df["leagueId"] = league["leagueId"] + if df is None: + df = currency_df + else: + df = pd.concat((df, currency_df)) + return df def store_data_to_csv(self, path: str) -> None: """ diff --git a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/data_retrieval/poe_api_handler.py b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/data_retrieval/poe_api_handler.py index 68440a14..ab500cb2 100644 --- a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/data_retrieval/poe_api_handler.py +++ b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/data_retrieval/poe_api_handler.py @@ -3,10 +3,10 @@ import time from collections.abc import Iterator from concurrent.futures import Future, ThreadPoolExecutor +from typing import Any import aiohttp import pandas as pd -import requests from data_retrieval_app.external_data_retrieval.config import settings from data_retrieval_app.external_data_retrieval.detectors.unique_detector import ( @@ -18,12 +18,13 @@ UniqueWeaponDetector, ) from data_retrieval_app.external_data_retrieval.utils import ( - ProgramRunTooLongException, + ProgramFinished, ProgramTooSlowException, WrongLeagueSetException, sync_timing_tracker, ) from data_retrieval_app.logs.logger import data_retrieval_logger as logger +from data_retrieval_app.utils import get_data_safe pd.options.mode.chained_assignment = None # default='warn' @@ -38,6 +39,7 @@ def __init__( url: str, auth_token: str, *, + leagues: list[dict[str, Any]], n_wanted_items: int = 100, n_unique_wanted_items: int = 5, item_detectors: list[UniqueDetector] | None = None, @@ -53,11 +55,11 @@ def __init__( logger.debug("Initializing PoEAPIHandler.") if item_detectors is None: item_detectors = [ - UniqueArmourDetector(), - UniqueJewelDetector(), - UniqueJewelleryDetector(), - UniqueWeaponDetector(), - UniqueUnidentifiedDetector(), + UniqueArmourDetector(leagues), + UniqueJewelDetector(leagues), + UniqueJewelleryDetector(leagues), + UniqueWeaponDetector(leagues), + UniqueUnidentifiedDetector(leagues), ] logger.debug("Item detectors set to: " + str(item_detectors)) self.url = url @@ -81,8 +83,7 @@ def __init__( self.skip_program_too_slow = False self._program_too_slow = False - self.time_of_launch = time.perf_counter() - self.run_program_for_n_seconds = settings.TIME_BETWEEN_RESTART + self._program_finished = False logger.info("PoEAPIHandler successfully initialized.") self.n_checkpoints_per_transfromation = ( @@ -172,21 +173,15 @@ def _get_latest_change_id(self) -> str: ): # For testing purposes, set manual next_change_id next_change_id = settings.NEXT_CHANGE_ID return next_change_id - try: - # Can't have authorization header, so we make a new header - headers = { - "User-Agent": f"OAuth pathofmodifiers/0.1.0 (contact: {settings.OATH_ACC_TOKEN_CONTACT_EMAIL}) StrictMode" - } - response = requests.get( - "https://www.pathofexile.com/api/trade/data/change-ids", - headers=headers, - ) - response.raise_for_status() - except Exception as e: - logger.error( - f"The following error occurred while making request _get_latest_change_id: {e}" - ) - raise e + # Can't have authorization header, so we make a new header + headers = { + "User-Agent": f"OAuth pathofmodifiers/0.1.0 (contact: {settings.OATH_ACC_TOKEN_CONTACT_EMAIL}) StrictMode" + } + response = get_data_safe( + "https://www.pathofexile.com/api/trade/data/change-ids", + headers=headers, + logger=logger, + ) response_json = response.json() next_change_id = response_json["psapi"] logger.info(f"Retrieved latest change id: {next_change_id}. Sleeping for 310s") @@ -221,6 +216,8 @@ async def _send_n_recursion_requests( return [] if self._program_too_slow: raise ProgramTooSlowException + if self._program_finished: + raise ProgramFinished waiting_for_next_id_lock.acquire() @@ -305,6 +302,12 @@ async def _send_n_recursion_requests( logger.exception( f"The following exception occured during {self._send_n_recursion_requests.__name__}: {e}" ) + raise + except ProgramTooSlowException: + pass + except ProgramFinished: + pass + finally: if waiting_for_next_id_lock.locked(): logger.info("Released lock after crash") if headers is not None: @@ -324,7 +327,6 @@ async def _send_n_recursion_requests( ) waiting_for_next_id_lock.release() - raise async def _follow_stream( self, @@ -347,6 +349,8 @@ async def _follow_stream( stashes = await self._send_n_recursion_requests( 5, session, waiting_for_next_id_lock, mini_batch_size ) + if self._program_finished: + return logger.debug(f"Thread {threading.get_ident()} finished 5 requests") stash_lock.acquire() self.stashes += stashes @@ -356,6 +360,10 @@ async def _follow_stream( stashes = [] stashes_ready_event.set() await asyncio.sleep(1) + except ProgramTooSlowException: + pass + except ProgramFinished: + pass except Exception as e: logger.info( f"The following exception occured during {self._follow_stream}: {e}" @@ -449,9 +457,15 @@ def set_program_too_slow(self): """ self._program_too_slow = True + def set_program_finished(self): + """ + Used to forceall threads to crash, letting the program shut down. + """ + self._program_finished = True + def initialize_data_stream_threads( self, executor: ThreadPoolExecutor, listeners: int, has_crashed: bool - ) -> dict[Future, str] | Future: + ) -> dict[Future, str] | Future | list[Future]: """ Creates the communication tools between threads and store them for later use. Gets the latest change id, and initializes the listeners. @@ -524,7 +538,3 @@ def dump_stream(self, track_progress: bool = True) -> Iterator[pd.DataFrame]: yield df.reset_index() del df logger.info("Finished transformation phase") - current_time = time.perf_counter() - time_since_launch = current_time - self.time_of_launch - if time_since_launch > self.run_program_for_n_seconds: - raise ProgramRunTooLongException diff --git a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/detectors/base.py b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/detectors/base.py index 4fd711b7..30b0c9fc 100644 --- a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/detectors/base.py +++ b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/detectors/base.py @@ -1,9 +1,9 @@ import inspect import time +from typing import Any import pandas as pd -from data_retrieval_app.external_data_retrieval.config import settings from data_retrieval_app.logs.logger import data_retrieval_logger as logger @@ -15,19 +15,17 @@ class DetectorBase: wanted_items = {} found_items = {} - def __init__(self, enable_pbar: bool = False) -> None: + def __init__( + self, leagues: list[dict[str, Any]], enable_pbar: bool = False + ) -> None: """ `self.n_unique_items_found` needs to be stored inbetween item detector sessions. """ self.n_unique_items_found = 0 + self.leagues = [league["name"] for league in leagues] self.prev_item_hashes_found = {} - self.leagues = [ - settings.CURRENT_SOFTCORE_LEAGUE, - settings.CURRENT_HARDCORE_LEAGUE, - ] - self.pbar_enabled = enable_pbar def _general_filter(self, df: pd.DataFrame) -> pd.DataFrame: diff --git a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/main.py b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/main.py index 5629a7a0..4a420ec0 100644 --- a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/main.py +++ b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/main.py @@ -1,14 +1,14 @@ from concurrent.futures import ( ALL_COMPLETED, - FIRST_EXCEPTION, + FIRST_COMPLETED, Future, ThreadPoolExecutor, wait, ) from io import StringIO +from typing import Any import pandas as pd -import requests from data_retrieval_app.external_data_retrieval.config import settings from data_retrieval_app.external_data_retrieval.data_retrieval.currency_api_handler import ( @@ -25,7 +25,7 @@ UniquePoEAPIDataTransformer, ) from data_retrieval_app.external_data_retrieval.utils import ( - ProgramRunTooLongException, + ProgramFinished, ProgramTooSlowException, ) from data_retrieval_app.logs.logger import external_data_retrieval_logger as logger @@ -33,16 +33,18 @@ from data_retrieval_app.pom_api_authentication import ( get_superuser_token_headers, ) +from data_retrieval_app.utils import find_hours_since_launch, get_data_safe class ContinuousDataRetrieval: auth_token = settings.POE_PUBLIC_STASHES_AUTH_TOKEN - current_league = settings.CURRENT_SOFTCORE_LEAGUE stash_tab_url = "https://api.pathofexile.com/public-stash-tabs" backend_base_url = settings.BACKEND_BASE_URL modifier_url = f"{backend_base_url}/modifier/" + active_league_url = f"{backend_base_url}/league/active_league/" item_base_type_url = f"{backend_base_url}/itemBaseType/" + currency_url = f"{backend_base_url}/currency/" pom_auth_headers = get_superuser_token_headers(backend_base_url) def __init__( @@ -50,30 +52,31 @@ def __init__( items_per_batch: int, data_transformers: dict[str, PoEAPIDataTransformerBase], ): - self.data_transformers = { - key: data_transformer() + self.leagues = self._get_leagues() + self.data_transformers: dict[str, PoEAPIDataTransformerBase] = { + key: data_transformer(self.leagues) for key, data_transformer in data_transformers.items() } self.poe_api_handler = PoEAPIHandler( url=self.stash_tab_url, auth_token=self.auth_token, + leagues=self.leagues, n_wanted_items=items_per_batch, n_unique_wanted_items=10, ) self.currency_api_handler = CurrencyAPIHandler( - url=f"https://api.poe.watch/exchange/ratios?league={self.current_league}&game=poe1" + url="https://api.poe.watch/exchange/ratios?league={league}&game=poe1" ) self.currency_transformer = TransformCurrencyAPIData() def _get_modifiers(self) -> dict[str, pd.DataFrame]: - response = requests.get(self.modifier_url, headers=self.pom_auth_headers) + response = get_data_safe( + self.modifier_url, headers=self.pom_auth_headers, logger=logger + ) # Check if the request was successful modifier_df = pd.DataFrame() - if response.status_code != 200: - logger.error(f"Recieved response code {response} when retrieving modifiers") - response.raise_for_status() # Load the JSON data into a pandas DataFrame json_io = StringIO(response.content.decode("utf-8")) modifier_df = pd.read_json(json_io, dtype=str) @@ -97,17 +100,21 @@ def _get_modifiers(self) -> dict[str, pd.DataFrame]: ] return modifier_dfs + def _get_leagues(self) -> list[dict[str, Any]]: + response = get_data_safe( + self.active_league_url, headers=self.pom_auth_headers, logger=logger + ) + leagues = response.json() + + return leagues + def _get_item_base_types(self) -> dict[str, int]: - response = requests.get(self.item_base_type_url, headers=self.pom_auth_headers) + response = get_data_safe( + self.item_base_type_url, headers=self.pom_auth_headers, logger=logger + ) item_base_type_mapped = {} item_base_types = [] - if response.status_code != 200: - logger.error( - f"Recieved response code {response} when retrieving item base types" - ) - response.raise_for_status() - item_base_types = response.json() if not isinstance(item_base_types, list): item_base_types = [item_base_types] @@ -146,9 +153,55 @@ def _categorize_new_items(self, df: pd.DataFrame) -> dict[str, pd.DataFrame]: return split_dfs - def _get_new_currency_data(self) -> pd.DataFrame: - currency_df = self.currency_api_handler.make_request() - currency_df = self.currency_transformer.transform_into_tables(currency_df) + def _get_new_currency_data(self, current_hours: dict[int, int]) -> pd.DataFrame: + league_ids = list(current_hours.keys()) + response = get_data_safe( + self.currency_url + "latest_hours/", + params={"league_ids": league_ids}, + headers=self.pom_auth_headers, + logger=logger, + ) + + latest_hours: dict[str, int] = response.json() + need_new_data = [] + need_old_data = [] + if latest_hours: + for league_id, latest_hour in latest_hours.items(): + league_id = int(league_id) + if ( + league_id in current_hours + and latest_hour == current_hours[league_id] + ): + need_old_data.append(league_id) + else: + need_new_data.append(league_id) + else: + need_new_data = league_ids + + currency_df = None + if need_old_data: + response = get_data_safe( + self.currency_url + "latest_currencies/", + params={"league_ids": need_old_data}, + headers=self.pom_auth_headers, + logger=logger, + ) + + currency_df = pd.DataFrame(response.json()) + + if need_new_data: + needed_leagues = [ + league for league in self.leagues if league["leagueId"] in need_new_data + ] + new_data = self.currency_api_handler.make_request(needed_leagues) + new_data = self.currency_transformer.transform_into_tables( + new_data, current_hours + ) + if currency_df is None: + currency_df = new_data + else: + currency_df = pd.concat((currency_df, new_data)) + return currency_df def _initialize_data_stream_threads( @@ -160,21 +213,37 @@ def _initialize_data_stream_threads( def _follow_data_dump_stream(self): try: + current_hours = find_hours_since_launch(self.leagues) + # Only need to refer to one league to see when a new hour starts + current_hour = current_hours[self.leagues[0]["leagueId"]] + next_hour = current_hour + 1 logger.info("Retrieving modifiers from db.") modifier_dfs = self._get_modifiers() item_base_types = self._get_item_base_types() + currency_df = self._get_new_currency_data(current_hours) get_df = self.poe_api_handler.dump_stream() - for i, df in enumerate(get_df): + while current_hour < next_hour: + df = next(get_df) split_dfs = self._categorize_new_items(df) - if i % 50 == 0: - currency_df = self._get_new_currency_data() for data_transformer_type in self.data_transformers: self.data_transformers[data_transformer_type].transform_into_tables( df=split_dfs[data_transformer_type], modifier_df=modifier_dfs[data_transformer_type], currency_df=currency_df.copy(deep=True), item_base_types=item_base_types, + current_hours=current_hours, ) + current_hours = find_hours_since_launch(self.leagues) + current_hour = current_hours[self.leagues[0]["leagueId"]] + for data_transformer_type in self.data_transformers: + self.data_transformers[data_transformer_type].end_of_hour_cleanup() + + raise ProgramFinished + + except ProgramFinished: + raise + except ProgramTooSlowException: + raise except Exception as e: logger.exception( f"The following exception occured during '_follow_data_dump_stream': {e}" @@ -185,28 +254,28 @@ def retrieve_data(self): logger.info("Program starting up.") logger.info("Initiating data stream.") max_workers = 3 - listeners = max_workers - 1 # minus one because of transformation threads - try: - with ThreadPoolExecutor(max_workers=max_workers) as executor: - futures = self._initialize_data_stream_threads( - executor, listeners=listeners - ) - follow_future = executor.submit(self._follow_data_dump_stream) - futures[follow_future] = "data_processing" - logger.info("Waiting for futures to crash.") - while True: - done_futures, not_done_futures = wait( - futures, return_when=FIRST_EXCEPTION - ) - crashed_future = list(done_futures)[0] - future_job = futures.pop(crashed_future) - logger.info( - f"The future '{future_job}' has crashed. Finding exception..." - ) + listeners = max( + max_workers - 1, 1 + ) # minus one because of transformation threads + with ThreadPoolExecutor(max_workers=max_workers) as executor: + futures = self._initialize_data_stream_threads( + executor, listeners=listeners + ) + follow_future = executor.submit(self._follow_data_dump_stream) + futures[follow_future] = "data_processing" + logger.info("Waiting for futures to crash.") + finished = False + while True: + if finished: + return + done_futures, _ = wait(futures, return_when=FIRST_COMPLETED) + while done_futures: + future = done_futures.pop() + future_job = futures.pop(future) + if future_job == "data_processing": - crashed_future_exception = crashed_future.exception() try: - raise crashed_future_exception + future.result() except ProgramTooSlowException: logger.info( f"The job '{future_job}' was too slow. Restarting..." @@ -215,39 +284,30 @@ def retrieve_data(self): wait(futures, return_when=ALL_COMPLETED) - raise ProgramTooSlowException - except ProgramRunTooLongException: + finished = True + except ProgramFinished: logger.info( - f"The job '{future_job}' has been running too long. Restarting..." + f"The job '{future_job}' has finished this cycle. Restarting..." ) - self.poe_api_handler.set_program_too_slow() + self.poe_api_handler.set_program_finished() wait(futures, return_when=ALL_COMPLETED) - raise ProgramRunTooLongException + finished = True except Exception: logger.exception( - f"The following exception occured in job '{future_job}': {crashed_future_exception}" + f"The following exception occured in job '{future_job}': {future.exception()}" ) follow_future = executor.submit( self._follow_data_dump_stream ) futures[follow_future] = "data_processing" elif future_job == "listener": - logger.exception(crashed_future.exception().with_traceback()) - raise crashed_future.exception() new_future = self._initialize_data_stream_threads( executor, listeners=1, has_crashed=True, ) futures[new_future] = "listener" - except ProgramTooSlowException: - logger.info("Program was too slow. Restarting...") - except ProgramRunTooLongException: - logger.info("Program has run too long. Restarting...") - except Exception as e: - logger.exception(f"The following exception occured: {e}") - raise e def main(): diff --git a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/transforming_data/transform_currency_api_data.py b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/transforming_data/transform_currency_api_data.py index c4fb87fd..8465d070 100644 --- a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/transforming_data/transform_currency_api_data.py +++ b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/transforming_data/transform_currency_api_data.py @@ -1,26 +1,9 @@ import pandas as pd -import requests from data_retrieval_app.external_data_retrieval.config import settings -from data_retrieval_app.external_data_retrieval.data_retrieval.currency_api_handler import ( - CurrencyAPIHandler, -) from data_retrieval_app.logs.logger import transform_logger as logger from data_retrieval_app.pom_api_authentication import get_superuser_token_headers -from data_retrieval_app.utils import find_hours_since_launch, insert_data - - -def load_currency_data(): - """ - Loads data from the poe.ninja currency API. - """ - poe_ninja_currency_api_handler = CurrencyAPIHandler( - url=f"https://poe.ninja/api/data/currencyoverview?league={settings.CURRENT_SOFTCORE_LEAGUE}&type=Currency" - ) - - currencies_df = poe_ninja_currency_api_handler.make_request() - - return currencies_df +from data_retrieval_app.utils import get_data_safe, insert_data class TransformCurrencyAPIData: @@ -41,16 +24,11 @@ def _get_name_to_trade_name_dict(self) -> dict: headers = { "User-Agent": f"OAuth pathofmodifiers/0.1.0 (contact: {settings.OATH_ACC_TOKEN_CONTACT_EMAIL}) StrictMode" } - try: - response = requests.get( - "https://www.pathofexile.com/api/trade/data/static", headers=headers - ) - response.raise_for_status() - except Exception as e: - logger.error( - f"The following error occurred while making request _get_latest_change_id: {e}" - ) - raise e + response = get_data_safe( + "https://www.pathofexile.com/api/trade/data/static", + headers=headers, + logger=logger, + ) response_json = response.json() result = response_json["result"] @@ -65,7 +43,7 @@ def _get_name_to_trade_name_dict(self) -> dict: return currencies def _transform_currency_table( - self, currency_df: pd.DataFrame, hours_since_launch: int + self, currency_df: pd.DataFrame, current_hours: dict[int, int] ) -> pd.DataFrame: """ Since a chaos orb is always worth one chaos orb, ninja does not include it in its price api. @@ -83,14 +61,18 @@ def _transform_currency_table( "name": ["Chaos Orb"], "chaos.chaosValue": [1], } - chaos_df = pd.DataFrame.from_dict(chaos_dict) - currency_df = pd.concat((currency_df, chaos_df), ignore_index=True) + for league_id in currency_df["leagueId"].unique(): + chaos_dict["leagueId"] = [league_id] + chaos_df = pd.DataFrame.from_dict(chaos_dict) + currency_df = pd.concat((currency_df, chaos_df), ignore_index=True) currency_df["tradeName"] = currency_df["name"].map( lambda name: self.name_to_trade_name.get(name, pd.NA) ) - currency_df["createdHoursSinceLaunch"] = hours_since_launch + currency_df["createdHoursSinceLaunch"] = currency_df["leagueId"].map( + current_hours + ) return currency_df def _clean_currency_table(self, currency_df: pd.DataFrame) -> pd.DataFrame: @@ -101,7 +83,7 @@ def _clean_currency_table(self, currency_df: pd.DataFrame) -> pd.DataFrame: currency_df = currency_df.drop( currency_df.columns.difference( - ["tradeName", "valueInChaos", "createdHoursSinceLaunch"] + ["tradeName", "valueInChaos", "createdHoursSinceLaunch", "leagueId"] ), axis=1, ) @@ -111,17 +93,11 @@ def _clean_currency_table(self, currency_df: pd.DataFrame) -> pd.DataFrame: return currency_df def _get_latest_currency_id_series(self, currency_df: pd.DataFrame) -> pd.Series: - try: - response = requests.get( - f"{self.base_url}/currency/latest_currency_id/", - headers=self.pom_api_headers, - ) - response.raise_for_status() - except Exception as e: - logger.error( - f"The following error occurred while making request _get_latest_currency_id_series: {e}" - ) - raise e + response = get_data_safe( + f"{self.base_url}/currency/latest_currency_id/", + headers=self.pom_api_headers, + logger=logger, + ) latest_currency_id = int(response.text) currency_id = pd.Series( @@ -130,13 +106,14 @@ def _get_latest_currency_id_series(self, currency_df: pd.DataFrame) -> pd.Series ) return currency_id - def transform_into_tables(self, currency_df: pd.DataFrame) -> pd.DataFrame: + def transform_into_tables( + self, currency_df: pd.DataFrame, current_hours: dict[int, int] + ) -> pd.DataFrame: """ Transforms the data into tables and transforms with help functions. """ - hours_since_launch = find_hours_since_launch() logger.debug("Transforming data into tables.") - currency_df = self._transform_currency_table(currency_df, hours_since_launch) + currency_df = self._transform_currency_table(currency_df, current_hours) logger.debug("Successfully transformed data into tables.") logger.debug("Cleaning currency table data.") diff --git a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/transforming_data/transform_poe_api_data.py b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/transforming_data/transform_poe_api_data.py index 70a0da35..493137cf 100644 --- a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/transforming_data/transform_poe_api_data.py +++ b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/transforming_data/transform_poe_api_data.py @@ -1,5 +1,6 @@ +from typing import Any + import pandas as pd -import requests from requests.exceptions import HTTPError from data_retrieval_app.external_data_retrieval.config import settings @@ -9,24 +10,27 @@ from data_retrieval_app.external_data_retrieval.utils import sync_timing_tracker from data_retrieval_app.logs.logger import transform_logger as logger from data_retrieval_app.pom_api_authentication import get_superuser_token_headers -from data_retrieval_app.utils import find_hours_since_launch, insert_data +from data_retrieval_app.utils import get_data_safe, insert_data pd.options.mode.chained_assignment = None # default="warn" class PoEAPIDataTransformerBase: - def __init__(self) -> None: + def __init__( + self, + leagues: list[dict[str, Any]], + ) -> None: logger.debug("Initializing PoEAPIDataTransformer") self.base_url = settings.BACKEND_BASE_URL self.pom_auth_headers = get_superuser_token_headers(self.base_url) self.roll_processor = RollProcessor() + self.league_to_id = {league["name"]: league["leagueId"] for league in leagues} + logger.debug("Initializing PoEAPIDataTransformer done.") - def _create_item_table( - self, df: pd.DataFrame, hours_since_launch: int - ) -> pd.DataFrame: + def _create_item_table(self, df: pd.DataFrame) -> pd.DataFrame: """ Creates the basis of the `item` table. """ @@ -64,11 +68,12 @@ def _create_item_table( :, [column for column in self.item_columns if column in df.columns] ] # Can't guarantee all columns are present - item_df["createdHoursSinceLaunch"] = hours_since_launch return item_df def _find_not_too_highly_priced_item_mask( - self, currency_df: pd.DataFrame, item_currency_merged_df: pd.DataFrame + self, + currency_df: pd.DataFrame, + item_currency_merged_df: pd.DataFrame, ) -> pd.Series: """ Some items are too highly priced to be legitimate. A boundary of the price equivelant to @@ -94,6 +99,7 @@ def _transform_item_table( item_df: pd.DataFrame, currency_df: pd.DataFrame, item_base_types: dict[str, int], + current_hours: dict[int, int], ) -> pd.DataFrame: """ The `item` table requires a foreign key to the `currency` table. @@ -129,6 +135,9 @@ def transform_influences(row: pd.DataFrame, influence_columns: list[str]): ] = True return influence_dict + item_df["leagueId"] = item_df["league"].map(self.league_to_id) + item_df["createdHoursSinceLaunch"] = item_df["leagueId"].map(current_hours) + base_type_series = item_df["baseType"] item_df["itemBaseTypeId"] = base_type_series.apply(transform_base_types) @@ -167,8 +176,8 @@ def transform_influences(row: pd.DataFrame, influence_columns: list[str]): item_df = item_df.merge( currency_df, how="left", - left_on="currencyType", - right_on="tradeName", + left_on=["currencyType", "leagueId"], + right_on=["tradeName", "leagueId"], suffixes=(None, "_y"), ) @@ -195,7 +204,7 @@ def item_table_columns_to_not_drop(self) -> set[str]: dont_drop_columns = { "gameItemId", "name", - "league", + "leagueId", "itemBaseTypeId", "ilvl", "rarity", @@ -244,16 +253,11 @@ def _clean_item_table(self, item_df: pd.DataFrame) -> pd.DataFrame: return item_df def _get_latest_item_id_series(self, item_df: pd.DataFrame) -> pd.Series: - try: - response = requests.get( - f"{self.base_url}/item/latest_item_id/", headers=self.pom_auth_headers - ) - response.raise_for_status() - except Exception as e: - logger.error( - f"The following error occurred while making request _get_latest_item_id_series: {e}" - ) - raise e + response = get_data_safe( + f"{self.base_url}/item/latest_item_id/", + headers=self.pom_auth_headers, + logger=logger, + ) latest_item_id = int(response.text) item_id = pd.Series( @@ -267,13 +271,15 @@ def _process_item_table( df: pd.DataFrame, currency_df: pd.DataFrame, item_base_types: dict[str, int], - hours_since_launch: int, + current_hours: dict[int, int], ) -> pd.Series: """ Needs to return item ids, as it is used to connect the item modifiers """ - item_df = self._create_item_table(df, hours_since_launch=hours_since_launch) - item_df = self._transform_item_table(item_df, currency_df, item_base_types) + item_df = self._create_item_table(df) + item_df = self._transform_item_table( + item_df, currency_df, item_base_types, current_hours=current_hours + ) item_df = self._clean_item_table(item_df) insert_data( item_df, @@ -286,6 +292,41 @@ def _process_item_table( logger.debug("Latest item id found: " + str(item_id)) return item_id + @sync_timing_tracker + def _transform_unidentified_item_table( + self, + item_df: pd.DataFrame, + currency_df: pd.DataFrame, + item_base_types: dict[str, int], + current_hours: dict[int, int], + ) -> pd.DataFrame: + """ + For convenience, all unid items are stored in divine prices + """ + item_df = self._transform_item_table( + item_df, currency_df, item_base_types, current_hours=current_hours + ) + + item_df["chaos_value"] = ( + item_df["currencyAmount"].astype(float) * item_df["valueInChaos"] + ) + + for league in item_df["leagueId"].unique(): + divine_row = currency_df.loc[ + (currency_df["leagueId"] == league) + & (currency_df["tradeName"] == "divine") + ].iloc[0] + + divine_id = divine_row["currencyId"] + divine_value = divine_row["valueInChaos"] + + item_league_mask = item_df["leagueId"] == league + item_df.loc[item_league_mask, "currencyId"] = divine_id + item_df.loc[item_league_mask, "currencyAmount"] = ( + item_df.loc[item_league_mask, "chaos_value"] / divine_value + ) + return item_df + def _clean_unidentified_item_table(self, item_df: pd.DataFrame) -> pd.DataFrame: """ Gets rid of unnecessay information, so that only fields needed for the DB remains. @@ -297,7 +338,7 @@ def _clean_unidentified_item_table(self, item_df: pd.DataFrame) -> pd.DataFrame: "name", "itemBaseTypeId", "createdHoursSinceLaunch", - "league", + "leagueId", "currencyId", "ilvl", "currencyAmount", @@ -316,15 +357,73 @@ def _clean_unidentified_item_table(self, item_df: pd.DataFrame) -> pd.DataFrame: return unidentified_item_df + def _aggregate_unidentified_item_table( + self, + ): + """ + Should run once at the end of every hour. For safety, it also aggregates all previous + hours, in case of previous unfortunate errors. + """ + response = get_data_safe( + f"{self.base_url}/unidentifiedItem/non_aggregated/", + headers=self.pom_auth_headers, + logger=logger, + ) + unid_df = pd.DataFrame(response.json()) + if unid_df.empty: + logger.info("Found no unidentified items to aggregate") + return + + group_cols = [ + "leagueId", + "name", + "itemBaseTypeId", + "createdHoursSinceLaunch", + "ilvl", + "identified", + "currencyId", + "rarity", + ] + + g = unid_df.groupby(group_cols) + + unid_df["calc_avg"] = g["currencyAmount"].transform("mean") + unid_df["calc_std"] = g["currencyAmount"].transform("std") + unid_df["calc_count"] = g["itemId"].transform("count") + + filtered_df = unid_df[ + unid_df["currencyAmount"].between( + unid_df["calc_avg"] - 1.97 * unid_df["calc_std"], + unid_df["calc_avg"] + 1.97 * unid_df["calc_std"], + ) + | unid_df["calc_std"].isna() + ] + + result_df = filtered_df.groupby(group_cols, as_index=False).agg( + currencyAmount=("currencyAmount", "mean"), + nItems=("calc_count", "first"), + ) + result_df["aggregated"] = True + logger.info("Pushing aggregated unidentified items") + insert_data( + result_df, + url=self.base_url, + table_name="unidentifiedItem/add_aggregated", + logger=logger, + headers=self.pom_auth_headers, + ) + def _process_unidentified_item_table( self, df: pd.DataFrame, currency_df: pd.DataFrame, item_base_types: dict[str, int], - hours_since_launch: int, + current_hours: dict[int, int], ) -> None: - item_df = self._create_item_table(df, hours_since_launch=hours_since_launch) - item_df = self._transform_item_table(item_df, currency_df, item_base_types) + item_df = self._create_item_table(df) + item_df = self._transform_unidentified_item_table( + item_df, currency_df, item_base_types, current_hours=current_hours + ) item_df = self._clean_unidentified_item_table(item_df) insert_data( item_df, @@ -335,7 +434,7 @@ def _process_unidentified_item_table( ) def _create_item_modifier_table( - self, df: pd.DataFrame, *, item_id: pd.Series, hours_since_launch: int + self, df: pd.DataFrame, *, item_id: pd.Series ) -> pd.DataFrame: """ The `item_modifier` table heavily relies on what type of item the modifiers @@ -346,6 +445,7 @@ def _create_item_modifier_table( def _transform_item_modifier_table( self, item_modifier_df: pd.DataFrame, + current_hours: dict[int, int], ) -> pd.DataFrame: """ The `item_modifier` table heavily relies on what type of item the modifiers @@ -368,14 +468,13 @@ def _process_item_modifier_table( self, df: pd.DataFrame, item_id: pd.Series, - hours_since_launch: int, + current_hours: dict[int, int], ) -> None: item_modifier_df = self._create_item_modifier_table( - df, item_id=item_id, hours_since_launch=hours_since_launch + df, item_id=item_id, current_hours=current_hours ) item_modifier_df = self._transform_item_modifier_table(item_modifier_df) item_modifier_df = self._clean_item_modifier_table(item_modifier_df) - insert_data( item_modifier_df, url=self.base_url, @@ -390,28 +489,28 @@ def transform_into_tables( modifier_df: pd.DataFrame, currency_df: pd.DataFrame, item_base_types: dict[str, int], + current_hours: dict[int, int], ) -> None: self.roll_processor.add_modifier_df(modifier_df) try: logger.debug("Transforming data into tables.") logger.debug("Processing data tables.") - hours_since_launch = find_hours_since_launch() item_id = self._process_item_table( df.copy(deep=True), currency_df=currency_df, item_base_types=item_base_types, - hours_since_launch=hours_since_launch, + current_hours=current_hours, ) self._process_unidentified_item_table( df.copy(deep=True), currency_df=currency_df, item_base_types=item_base_types, - hours_since_launch=hours_since_launch, + current_hours=current_hours, ) self._process_item_modifier_table( df.copy(deep=True), item_id=item_id, - hours_since_launch=hours_since_launch, + current_hours=current_hours, ) logger.debug("Successfully transformed data into tables.") @@ -419,17 +518,24 @@ def transform_into_tables( logger.exception(f"Something went wrong:\n{repr(e)}") raise e + def end_of_hour_cleanup(self): + self._aggregate_unidentified_item_table() + class UniquePoEAPIDataTransformer(PoEAPIDataTransformerBase): @sync_timing_tracker def _create_item_modifier_table( - self, df: pd.DataFrame, *, item_id: pd.Series, hours_since_launch: int + self, + df: pd.DataFrame, + *, + item_id: pd.Series, + current_hours: dict[int, int], ) -> pd.DataFrame: """ A similiar process to creating the item table, only this time the relevant column contains a list and not a JSON-object """ - item_modifier_columns = ["name", "explicitMods"] + item_modifier_columns = ["name", "explicitMods", "league"] item_modifier_df = df.loc[ self.price_found_mask, item_modifier_columns, @@ -440,19 +546,23 @@ def _create_item_modifier_table( ].reset_index() item_modifier_df["itemId"] = item_id + item_modifier_df["leagueId"] = item_modifier_df["league"].map(self.league_to_id) + item_modifier_df["createdHoursSinceLaunch"] = item_modifier_df["leagueId"].map( + current_hours + ) item_modifier_df = item_modifier_df.explode("explicitMods", ignore_index=True) item_modifier_df.rename({"explicitMods": "modifier"}, axis=1, inplace=True) - item_modifier_df["createdHoursSinceLaunch"] = hours_since_launch - return item_modifier_df @sync_timing_tracker def _transform_item_modifier_table( - self, item_modifier_df: pd.DataFrame + self, + item_modifier_df: pd.DataFrame, ) -> pd.DataFrame: item_modifier_df = self.roll_processor.add_rolls(df=item_modifier_df) + return item_modifier_df @property diff --git a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/utils.py b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/utils.py index b038c2d2..00d94960 100644 --- a/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/utils.py +++ b/src/backend_data_retrieval/data_retrieval_app/external_data_retrieval/utils.py @@ -79,7 +79,7 @@ class ProgramTooSlowException(Exception): pass -class ProgramRunTooLongException(Exception): +class ProgramFinished(Exception): pass diff --git a/src/backend_data_retrieval/data_retrieval_app/tests/data_deposit/league/__init__.py b/src/backend_data_retrieval/data_retrieval_app/tests/data_deposit/league/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/backend_data_retrieval/data_retrieval_app/tests/data_deposit/league/test_league_depositor.py b/src/backend_data_retrieval/data_retrieval_app/tests/data_deposit/league/test_league_depositor.py new file mode 100644 index 00000000..cae116f6 --- /dev/null +++ b/src/backend_data_retrieval/data_retrieval_app/tests/data_deposit/league/test_league_depositor.py @@ -0,0 +1,3 @@ +class TestLeagueDataDepositor: + # TODO: Add tests if needed + pass diff --git a/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/config.py b/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/config.py index 46c18da7..3f84eb83 100644 --- a/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/config.py +++ b/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/config.py @@ -1,4 +1,3 @@ -from pydantic import computed_field from pydantic_settings import BaseSettings @@ -26,17 +25,5 @@ def dispersed_timing_enabled(self) -> bool: ] # , "divine", "mirror", etc. MEAN_ITEM_PRICE: int = 200 - ALL_SOFTCORE_LEAGUES: str - - @computed_field # type: ignore[prop-decorator] - @property - def SOFTCORE_LEAGUES(self) -> list[str]: - return self.ALL_SOFTCORE_LEAGUES.split("|") - - @computed_field # type: ignore[prop-decorator] - @property - def HARDCORE_LEAGUES(self) -> list[str]: - return [f"Hardcore {league}" for league in self.SOFTCORE_LEAGUES] - script_settings = PoEPublicStashesTestDataSettings() diff --git a/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/main.py b/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/main.py index e01e59c4..7e60fd7d 100644 --- a/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/main.py +++ b/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/main.py @@ -21,7 +21,8 @@ class PublicStashMockAPI: - def __init__(self) -> None: + def __init__(self, leagues: list[dict[str, Any]]) -> None: + self.leagues = [league["name"] for league in leagues] scrap_and_mock_poe_api_docs_objs = ScrapAndMockPoEAPIDocs() ( self.public_stashes_mock_obj, @@ -29,13 +30,9 @@ def __init__(self) -> None: ) = scrap_and_mock_poe_api_docs_objs.produce_mocks_from_docs() self.public_stashes_modifier_test_data_creator = DataDepositTestDataCreator( - n_of_items=script_settings.N_OF_ITEMS_PER_MODIFIER_FILE + n_of_items=script_settings.N_OF_ITEMS_PER_MODIFIER_FILE, leagues=leagues ) self.public_stashes_modifier_test_data_creator.create_templates() - self.leagues = [ - *script_settings.SOFTCORE_LEAGUES, - *script_settings.HARDCORE_LEAGUES, - ] def get_test_data(self) -> list[dict[str, Any]]: """Generates test data by modifying mock stashes and items based on league.""" diff --git a/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/utils/data_deposit_test_data_creator.py b/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/utils/data_deposit_test_data_creator.py index de8694d4..5328f1fc 100644 --- a/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/utils/data_deposit_test_data_creator.py +++ b/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/utils/data_deposit_test_data_creator.py @@ -5,7 +5,6 @@ from typing import Any import pandas as pd -import requests from data_retrieval_app.external_data_retrieval.config import settings from data_retrieval_app.logs.logger import test_logger @@ -16,10 +15,11 @@ script_settings, ) from data_retrieval_app.tests.utils import random_float, random_int +from data_retrieval_app.utils import get_data_safe class DataDepositTestDataCreator: - def __init__(self, n_of_items: int) -> None: + def __init__(self, n_of_items: int, leagues: list[dict[str, Any]]) -> None: """ Creates a number of items from the data deposit files. @@ -34,10 +34,7 @@ def __init__(self, n_of_items: int) -> None: self.base_url = settings.BACKEND_BASE_URL self.pom_auth_headers = get_superuser_token_headers(self.base_url) - self.leagues = [ - *script_settings.SOFTCORE_LEAGUES, - *script_settings.HARDCORE_LEAGUES, - ] + self.leagues = [league["name"] for league in leagues] self.base_type_df = self._get_df_from_url("itemBaseType/") self.grouped_modifier_df = self._get_df_from_url( @@ -59,7 +56,7 @@ def __init__(self, n_of_items: int) -> None: def _get_df_from_url(self, route: str) -> pd.DataFrame: headers = {"accept": "application/json", "Content-Type": "application/json"} headers.update(self.pom_auth_headers) - response = requests.get(f"{self.base_url}/{route}", headers=headers) + response = get_data_safe(f"{self.base_url}/{route}", headers=headers) df = pd.read_json(StringIO(response.text), orient="records") return df @@ -425,7 +422,8 @@ def create_test_data( def main() -> int: - test = DataDepositTestDataCreator(50) + leagues = [{"name": "testing_league"}] + test = DataDepositTestDataCreator(50, leagues=leagues) test.create_templates() print(list(test.create_test_data())) return 0 diff --git a/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/utils/scrap_and_mock_poe_api_docs_objs.py b/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/utils/scrap_and_mock_poe_api_docs_objs.py index fbbda98e..0bf398a9 100644 --- a/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/utils/scrap_and_mock_poe_api_docs_objs.py +++ b/src/backend_data_retrieval/data_retrieval_app/tests/scripts/create_public_stashes_test_data/utils/scrap_and_mock_poe_api_docs_objs.py @@ -2,9 +2,10 @@ import uuid from typing import Any -import requests from bs4 import BeautifulSoup +from data_retrieval_app.utils import get_data_safe + class ScrapAndMockPoEAPIDocs: def __init__(self) -> None: @@ -19,8 +20,7 @@ def _fetch_schema_from_web(self, url): headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36" } - response = requests.get(url, headers=headers) - response.raise_for_status() + response = get_data_safe(url, headers=headers) return response.text # Parse the schema table using BeautifulSoup diff --git a/src/backend_data_retrieval/data_retrieval_app/tests/scripts/test_with_sim_api.py b/src/backend_data_retrieval/data_retrieval_app/tests/scripts/test_with_sim_api.py index 131f8160..9ecfedb1 100644 --- a/src/backend_data_retrieval/data_retrieval_app/tests/scripts/test_with_sim_api.py +++ b/src/backend_data_retrieval/data_retrieval_app/tests/scripts/test_with_sim_api.py @@ -1,4 +1,5 @@ import threading +from typing import Any import numpy as np import pandas as pd @@ -18,26 +19,29 @@ from data_retrieval_app.tests.scripts.create_public_stashes_test_data.main import ( PublicStashMockAPI, ) -from data_retrieval_app.utils import find_hours_since_launch class TestUniquePoEAPIDataTransformer(UniquePoEAPIDataTransformer): - def __init__(self): - super().__init__() + def __init__(self, leagues: dict[str, Any]): + super().__init__(leagues) def _create_random_time_column(self, length: int) -> pd.Series: - start = find_hours_since_launch() - random_times = np.random.randint(0, script_settings.TIMING_PERIOD * 24, length) - time_column = start - random_times - - return pd.Series(time_column, dtype=str) + return pd.Series(random_times, dtype=int) - def _create_item_table(self, df, hours_since_launch: int): - item_df = super()._create_item_table(df, hours_since_launch) + def _transform_item_table( + self, + item_df: pd.DataFrame, + currency_df: pd.DataFrame, + item_base_types: dict[str, int], + current_hours: dict[int, int], + ) -> pd.DataFrame: + item_df = super()._transform_item_table( + item_df, currency_df, item_base_types, current_hours + ) if script_settings.dispersed_timing_enabled: - item_df["createdHoursSinceLaunch"] = self.time_column + item_df["createdHoursSinceLaunch"] += self.time_column return item_df @@ -46,18 +50,15 @@ def _create_item_modifier_table( df: pd.DataFrame, *, item_id: pd.Series, - hours_since_launch: int, # noqa: ARG003 + current_hours: dict[int, int], ) -> pd.DataFrame: """ A similiar process to creating the item table, only this time the relevant column contains a list and not a JSON-object """ - item_modifier_columns = ["name", "explicitMods"] - - if script_settings.dispersed_timing_enabled: - df["createdHoursSinceLaunch"] = self.time_column - item_modifier_columns.append("createdHoursSinceLaunch") - + # This method does the exact same thing, except adding random time if enabled + # This needs to be done before explode, but after filtering extremly priced items + item_modifier_columns = ["name", "explicitMods", "league"] item_modifier_df = df.loc[ self.price_found_mask, item_modifier_columns, @@ -68,10 +69,13 @@ def _create_item_modifier_table( ].reset_index() item_modifier_df["itemId"] = item_id - item_modifier_df = item_modifier_df.explode("explicitMods", ignore_index=True) - test_logger.info( - f"Transforming {len(item_modifier_df)} item modifiers, making them ready to insert into db" + item_modifier_df["leagueId"] = item_modifier_df["league"].map(self.league_to_id) + item_modifier_df["createdHoursSinceLaunch"] = item_modifier_df["leagueId"].map( + current_hours ) + if script_settings.dispersed_timing_enabled: + item_modifier_df["createdHoursSinceLaunch"] += self.time_column + item_modifier_df = item_modifier_df.explode("explicitMods", ignore_index=True) item_modifier_df.rename({"explicitMods": "modifier"}, axis=1, inplace=True) @@ -83,6 +87,7 @@ def transform_into_tables( modifier_df: pd.DataFrame, currency_df: pd.DataFrame, item_base_types: dict[str, int], + current_hours: dict[int, int], ) -> None: if script_settings.dispersed_timing_enabled: self.time_column = self._create_random_time_column(length=len(df)) @@ -90,17 +95,15 @@ def transform_into_tables( test_logger.info( f"Transforming {len(df)} items, making them ready to insert into db" ) - super().transform_into_tables(df, modifier_df, currency_df, item_base_types) + super().transform_into_tables( + df, modifier_df, currency_df, item_base_types, current_hours + ) class PoEMockAPIHandler(PoEAPIHandler): def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - - self.mock_api = PublicStashMockAPI() - - for detector in self.item_detectors: - detector.leagues = self.mock_api.leagues + self.mock_api = PublicStashMockAPI(leagues=kwargs["leagues"]) self.run_program_for_n_seconds = script_settings.CREATE_TEST_DATA_FOR_N_SECONDS self.n_checkpoints_per_transfromation = ( @@ -137,6 +140,7 @@ def __init__( self.poe_api_handler = PoEMockAPIHandler( url=self.stash_tab_url, auth_token=self.auth_token, + leagues=self.leagues, n_wanted_items=items_per_batch, n_unique_wanted_items=10, ) diff --git a/src/backend_data_retrieval/data_retrieval_app/utils.py b/src/backend_data_retrieval/data_retrieval_app/utils.py index e45cbc0e..0e0d6f89 100644 --- a/src/backend_data_retrieval/data_retrieval_app/utils.py +++ b/src/backend_data_retrieval/data_retrieval_app/utils.py @@ -7,7 +7,6 @@ import requests from pydantic import HttpUrl -from data_retrieval_app.external_data_retrieval.config import settings from data_retrieval_app.logs.logger import main_logger as logger @@ -54,19 +53,25 @@ def df_to_JSON( ) -def find_hours_since_launch() -> int: +def find_hours_since_launch(leagues_df: list[dict]) -> dict[int, int]: + """ + Finds the number of hours since launch for each of the leagues in the given dataframe + """ current_time = datetime.now(UTC) - league_launch_time = settings.LEAGUE_LAUNCH_DATETIME_OBJECT + hours_since_launch_dict = {} + for league in leagues_df: + league_launch_time = datetime.fromisoformat(league["validFrom"]) - time_since_launch = current_time - league_launch_time + time_since_launch = current_time - league_launch_time - days_since_launch, seconds_since_launch = ( - time_since_launch.days, - time_since_launch.seconds, - ) - hours_since_launch = days_since_launch * 24 + seconds_since_launch // 3600 + days_since_launch, seconds_since_launch = ( + time_since_launch.days, + time_since_launch.seconds, + ) + hours_since_launch = days_since_launch * 24 + seconds_since_launch // 3600 + hours_since_launch_dict[league["leagueId"]] = hours_since_launch - return hours_since_launch + return hours_since_launch_dict def insert_data( @@ -122,3 +127,23 @@ def insert_data( f"Recieved a {response.status_code} response. Error msg: {response.text[:10000]}" ) response.raise_for_status() + + +def get_data_safe( + url: str, + *, + logger: logging.Logger = None, + params: dict | list = None, + headers: dict[str, str] | None = None, +) -> requests.Response: + try: + response = requests.get(url, params=params, headers=headers) + response.raise_for_status() + except Exception as e: + if logger is not None: + logger.error( + f"The following error occurred while making request a request to {url}: {e}" + ) + raise e + + return response diff --git a/src/backend_data_retrieval/prestart.sh b/src/backend_data_retrieval/prestart.sh index 799ec4e8..a3a406ce 100755 --- a/src/backend_data_retrieval/prestart.sh +++ b/src/backend_data_retrieval/prestart.sh @@ -28,10 +28,6 @@ if [[ -z "${POE_PUBLIC_STASHES_AUTH_TOKEN}" ]] || [[ "${POE_PUBLIC_STASHES_AUTH_ echo "Env variable POE_PUBLIC_STASHES_AUTH_TOKEN is not set" exit 1 fi -if [[ -z "${CURRENT_SOFTCORE_LEAGUE}" ]] || [[ "${CURRENT_SOFTCORE_LEAGUE}" == "changethis" ]]; then - echo "Env variable CURRENT_SOFTCORE_LEAGUE is not set" - exit 1 -fi if [[ -z "${OATH_ACC_TOKEN_CONTACT_EMAIL}" ]] || [[ "${OATH_ACC_TOKEN_CONTACT_EMAIL}" == "changethis" ]]; then echo "Env variable OATH_ACC_TOKEN_CONTACT_EMAIL is not set" exit 1 diff --git a/src/frontend/.env b/src/frontend/.env index bf572249..a54216fe 100644 --- a/src/frontend/.env +++ b/src/frontend/.env @@ -1,6 +1,3 @@ VITE_API_URL=http://localhost:8000 # This is the URL for the FastAPI backend -VITE_APP_DEFAULT_LEAGUES="Mirage|Keepers|Mercenaries|Phrecia" # This is the default leagues, the first one is the current league to show on the frontend -VITE_APP_ADDITIONAL_LEAGUES="Hardcore Mirage|Hardcore Keepers|Hardcore Mercenaries|Hardcore Phrecia" # NB: Separate by '|' for each league VITE_APP_TURNSTILE_SITE_KEY=1x00000000000000000000AA # This is the site key for the Turnstile captcha -VITE_APP_LEAGUE_LAUNCH_TIME=2026-03-06T19:00:00Z # ISO 8601 format. Round backwards to whole hour number VITE_APP_PLOTTING_WINDOW_HOURS=336 diff --git a/src/frontend/openapi.json b/src/frontend/openapi.json index fdf3cf20..6bd2aaab 100644 --- a/src/frontend/openapi.json +++ b/src/frontend/openapi.json @@ -7,9 +7,7 @@ "paths": { "/api/api_v1/currency/{currencyId}": { "get": { - "tags": [ - "currencys" - ], + "tags": ["currency"], "summary": "Get Currency", "description": "Get currency by key and value for \"currencyId\".\n\nAlways returns one currency.", "operationId": "get_currency", @@ -51,11 +49,210 @@ } } } + } + }, + "/api/api_v1/currency/": { + "get": { + "tags": ["currency"], + "summary": "Get All Currencies", + "description": "Get all currencies.\n\nReturns a list of all currencies.", + "operationId": "get_all_currencies", + "security": [ + { + "OAuth2PasswordBearer": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0 + }, + { + "type": "null" + } + ], + "title": "Limit" + } + }, + { + "name": "skip", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Skip" + } + }, + { + "name": "sort_key", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sort Key" + } + }, + { + "name": "sort_method", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "enum": ["asc", "desc"], + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Sort Method" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Currency" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" + } + } + ], + "title": "Response Currency-Get All Currencies" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } }, - "put": { - "tags": [ - "currencys" + "post": { + "tags": ["currency"], + "summary": "Create Currency", + "description": "Create one or a list of currencies.\n\nReturns the created currency or list of currencies.", + "operationId": "create_currency", + "security": [ + { + "OAuth2PasswordBearer": [] + } + ], + "parameters": [ + { + "name": "return_nothing", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "Return Nothing" + } + } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/CurrencyCreate" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/CurrencyCreate" + } + } + ], + "title": "Currency" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/CurrencyCreate" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/CurrencyCreate" + } + }, + { + "type": "null" + } + ], + "title": "Response Currency-Create Currency" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "put": { + "tags": ["currency"], "summary": "Update Currency", "description": "Update a currency by key and value for \"currencyId\".\n\nReturns the updated currency.", "operationId": "update_currency", @@ -67,7 +264,61 @@ "parameters": [ { "name": "currencyId", - "in": "path", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "title": "Currencyid" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CurrencyUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Currency" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["currency"], + "summary": "Delete Currency", + "description": "Delete a currency by key and value for \"currencyId\".\n\nReturns a message indicating the currency was deleted.\nAlways deletes one currency.", + "operationId": "delete_currency", + "security": [ + { + "OAuth2PasswordBearer": [] + } + ], + "parameters": [ + { + "name": "currencyId", + "in": "query", "required": true, "schema": { "type": "integer", @@ -75,15 +326,187 @@ } } ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string", + "title": "Response Currency-Delete Currency" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/api_v1/currency/latest_currency_id/": { + "get": { + "tags": ["currency", "latest_currency_id"], + "summary": "Get Latest Currency Id", + "description": "Get the latest currencyId, returns 1 if table is empty\n\nCan only be used safely on an empty table or directly after an insertion.", + "operationId": "get_latest_currency_id", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "integer", + "title": "Response Currency-Get Latest Currency Id" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/api_v1/currency/latest_hours/": { + "get": { + "tags": ["currency", "latest_hours"], + "summary": "Get Latest Hours", + "description": "Returns a dict mapping league id to the most recent hour relating to that league. Dict is empty if no currencies exist.\n\nDoes not guarantee an entry for every league id.", + "operationId": "get_latest_hours", + "security": [ + { + "OAuth2PasswordBearer": [] + } + ], + "parameters": [ + { + "name": "league_ids", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1, + "title": "League Ids" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer" + }, + "title": "Response Currency-Get Latest Hours" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/api_v1/currency/latest_currencies/": { + "get": { + "tags": ["currency", "latest_currencies"], + "summary": "Get Latest Currencies", + "description": "Returns a dict mapping league id to most recent currencies relating to that league. Dict is empty if no currencies exist.\n\nDoes not guarantee an entry for every league id.", + "operationId": "get_latest_currencies", + "security": [ + { + "OAuth2PasswordBearer": [] + } + ], + "parameters": [ + { + "name": "league_ids", + "in": "query", + "required": true, + "schema": { + "type": "array", + "items": { + "type": "integer" + }, + "minItems": 1, + "title": "League Ids" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Currency" + } + }, + "title": "Response Currency-Get Latest Currencies" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/api_v1/currency/from_query/": { + "post": { + "tags": ["currency"], + "summary": "Get Currency From Query", + "description": "Returns a list of currencies that match any of the queries", + "operationId": "get_currency_from_query", "requestBody": { - "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CurrencyUpdate" + "items": { + "$ref": "#/components/schemas/CurrencyQuery" + }, + "type": "array", + "title": "Query List" } } - } + }, + "required": true }, "responses": { "200": { @@ -91,7 +514,11 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Currency" + "items": { + "$ref": "#/components/schemas/Currency" + }, + "type": "array", + "title": "Response Currency-Get Currency From Query" } } } @@ -106,15 +533,20 @@ } } } - } - }, - "delete": { - "tags": [ - "currencys" - ], - "summary": "Delete Currency", - "description": "Delete a currency by key and value for \"currencyId\".\n\nReturns a message indicating the currency was deleted.\nAlways deletes one currency.", - "operationId": "delete_currency", + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/api_v1/itemBaseType/{itemBaseTypeId}": { + "get": { + "tags": ["itemBaseType"], + "summary": "Get Item Base Type", + "description": "Get item base type by key and value for \"itemBaseTypeId\".\n\nAlways returns one item base type.", + "operationId": "get_item_base_type", "security": [ { "OAuth2PasswordBearer": [] @@ -122,12 +554,12 @@ ], "parameters": [ { - "name": "currencyId", + "name": "itemBaseTypeId", "in": "path", "required": true, "schema": { - "type": "integer", - "title": "Currencyid" + "type": "string", + "title": "Itembasetypeid" } } ], @@ -137,8 +569,7 @@ "content": { "application/json": { "schema": { - "type": "string", - "title": "Response Currencys-Delete Currency" + "$ref": "#/components/schemas/ItemBaseType" } } } @@ -156,19 +587,12 @@ } } }, - "/api/api_v1/currency/": { + "/api/api_v1/itemBaseType/": { "get": { - "tags": [ - "currencys" - ], - "summary": "Get All Currencies", - "description": "Get all currencies.\n\nReturns a list of all currencies.", - "operationId": "get_all_currencies", - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], + "tags": ["itemBaseType"], + "summary": "Get All Item Base Types", + "description": "Get all item base types.\n\nReturns a list of all item base types.", + "operationId": "get_all_item_base_types", "parameters": [ { "name": "limit", @@ -227,10 +651,7 @@ "schema": { "anyOf": [ { - "enum": [ - "asc", - "desc" - ], + "enum": ["asc", "desc"], "type": "string" }, { @@ -249,16 +670,16 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/Currency" + "$ref": "#/components/schemas/ItemBaseType" }, { "type": "array", "items": { - "$ref": "#/components/schemas/Currency" + "$ref": "#/components/schemas/ItemBaseType" } } ], - "title": "Response Currencys-Get All Currencies" + "title": "Response Itembasetype-Get All Item Base Types" } } } @@ -276,18 +697,32 @@ } }, "post": { - "tags": [ - "currencys" - ], - "summary": "Create Currency", - "description": "Create one or a list of currencies.\n\nReturns the created currency or list of currencies.", - "operationId": "create_currency", + "tags": ["itemBaseType"], + "summary": "Create Item Base Type", + "description": "Create one or a list of new item base types.\n\nReturns the created item base type or list of item base types.", + "operationId": "create_item_base_type", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ + { + "name": "on_duplicate_pkey_do_nothing", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "title": "On Duplicate Pkey Do Nothing" + } + }, { "name": "return_nothing", "in": "query", @@ -312,16 +747,16 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/CurrencyCreate" + "$ref": "#/components/schemas/ItemBaseTypeCreate" }, { "type": "array", "items": { - "$ref": "#/components/schemas/CurrencyCreate" + "$ref": "#/components/schemas/ItemBaseTypeCreate" } } ], - "title": "Currency" + "title": "Itembasetype" } } } @@ -334,96 +769,19 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/CurrencyCreate" + "$ref": "#/components/schemas/ItemBaseTypeCreate" }, { "type": "array", "items": { - "$ref": "#/components/schemas/CurrencyCreate" + "$ref": "#/components/schemas/ItemBaseTypeCreate" } }, { "type": "null" } ], - "title": "Response Currencys-Create Currency" - } - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/api/api_v1/currency/latest_currency_id/": { - "get": { - "tags": [ - "currencys", - "latest_currency_id" - ], - "summary": "Get Latest Currency Id", - "description": "Get the latest currencyId\n\nCan only be used safely on an empty table or directly after an insertion.", - "operationId": "get_latest_currency_id", - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "type": "integer", - "title": "Response Currencys-Get Latest Currency Id" - } - } - } - } - }, - "security": [ - { - "OAuth2PasswordBearer": [] - } - ] - } - }, - "/api/api_v1/itemBaseType/{itemBaseTypeId}": { - "get": { - "tags": [ - "itemBaseTypes" - ], - "summary": "Get Item Base Type", - "description": "Get item base type by key and value for \"itemBaseTypeId\".\n\nAlways returns one item base type.", - "operationId": "get_item_base_type", - "security": [ - { - "OAuth2PasswordBearer": [] - } - ], - "parameters": [ - { - "name": "itemBaseTypeId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "title": "Itembasetypeid" - } - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ItemBaseType" + "title": "Response Itembasetype-Create Item Base Type" } } } @@ -441,9 +799,7 @@ } }, "put": { - "tags": [ - "itemBaseTypes" - ], + "tags": ["itemBaseType"], "summary": "Update Item Base Type", "description": "Update an item base type by key and value for \"itemBaseTypeId\".\n\nReturns the updated item base type.", "operationId": "update_item_base_type", @@ -455,7 +811,7 @@ "parameters": [ { "name": "itemBaseTypeId", - "in": "path", + "in": "query", "required": true, "schema": { "type": "integer", @@ -497,9 +853,7 @@ } }, "delete": { - "tags": [ - "itemBaseTypes" - ], + "tags": ["itemBaseType"], "summary": "Delete Item Base Type", "description": "Delete an item base type by key and value for \"itemBaseTypeId\".\n\nReturns a message that the item base type was deleted successfully.\nAlways deletes one item base type.", "operationId": "delete_item_base_type", @@ -511,7 +865,7 @@ "parameters": [ { "name": "itemBaseTypeId", - "in": "path", + "in": "query", "required": true, "schema": { "type": "integer", @@ -526,7 +880,7 @@ "application/json": { "schema": { "type": "string", - "title": "Response Itembasetypes-Delete Item Base Type" + "title": "Response Itembasetype-Delete Item Base Type" } } } @@ -544,14 +898,17 @@ } } }, - "/api/api_v1/itemBaseType/": { + "/api/api_v1/itemModifier/": { "get": { - "tags": [ - "itemBaseTypes" + "tags": ["itemModifier"], + "summary": "Get All Item Modifiers", + "description": "Get all item modifiers.\n\nReturns a list of all item modifiers.", + "operationId": "get_all_item_modifiers", + "security": [ + { + "OAuth2PasswordBearer": [] + } ], - "summary": "Get All Item Base Types", - "description": "Get all item base types.\n\nReturns a list of all item base types.", - "operationId": "get_all_item_base_types", "parameters": [ { "name": "limit", @@ -610,10 +967,7 @@ "schema": { "anyOf": [ { - "enum": [ - "asc", - "desc" - ], + "enum": ["asc", "desc"], "type": "string" }, { @@ -632,16 +986,16 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/ItemBaseType" + "$ref": "#/components/schemas/ItemModifier" }, { "type": "array", "items": { - "$ref": "#/components/schemas/ItemBaseType" + "$ref": "#/components/schemas/ItemModifier" } } ], - "title": "Response Itembasetypes-Get All Item Base Types" + "title": "Response Itemmodifier-Get All Item Modifiers" } } } @@ -659,34 +1013,16 @@ } }, "post": { - "tags": [ - "itemBaseTypes" - ], - "summary": "Create Item Base Type", - "description": "Create one or a list of new item base types.\n\nReturns the created item base type or list of item base types.", - "operationId": "create_item_base_type", + "tags": ["itemModifier"], + "summary": "Create Item Modifier", + "description": "Create one or a list item modifiers.\n\nReturns the created item modifier or list of item modifiers.", + "operationId": "create_item_modifier", "security": [ { "OAuth2PasswordBearer": [] } ], "parameters": [ - { - "name": "on_duplicate_pkey_do_nothing", - "in": "query", - "required": false, - "schema": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "null" - } - ], - "title": "On Duplicate Pkey Do Nothing" - } - }, { "name": "return_nothing", "in": "query", @@ -711,16 +1047,16 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/ItemBaseTypeCreate" + "$ref": "#/components/schemas/ItemModifierCreate" }, { "type": "array", "items": { - "$ref": "#/components/schemas/ItemBaseTypeCreate" + "$ref": "#/components/schemas/ItemModifierCreate" } } ], - "title": "Itembasetype" + "title": "Itemmodifier" } } } @@ -733,19 +1069,19 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/ItemBaseTypeCreate" + "$ref": "#/components/schemas/ItemModifierCreate" }, { "type": "array", "items": { - "$ref": "#/components/schemas/ItemBaseTypeCreate" + "$ref": "#/components/schemas/ItemModifierCreate" } }, { "type": "null" } ], - "title": "Response Itembasetypes-Create Item Base Type" + "title": "Response Itemmodifier-Create Item Modifier" } } } @@ -763,14 +1099,45 @@ } } }, - "/api/api_v1/itemModifier/": { + "/api/api_v1/item/latest_item_id/": { "get": { - "tags": [ - "itemModifiers" - ], - "summary": "Get All Item Modifiers", - "description": "Get all item modifiers.\n\nReturns a list of all item modifiers.", - "operationId": "get_all_item_modifiers", + "tags": ["item", "latest_item_id"], + "summary": "Get Latest Item Id", + "description": "Get the latest \"itemId\"\n\nCan only be used safely on an empty table or directly after an insertion.", + "operationId": "get_latest_item_id", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Response Item-Get Latest Item Id" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/api_v1/item/": { + "get": { + "tags": ["item"], + "summary": "Get All Items", + "description": "Get all items.\n\nReturns a list of all items.", + "operationId": "get_all_items", "security": [ { "OAuth2PasswordBearer": [] @@ -834,10 +1201,7 @@ "schema": { "anyOf": [ { - "enum": [ - "asc", - "desc" - ], + "enum": ["asc", "desc"], "type": "string" }, { @@ -856,16 +1220,16 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/ItemModifier" + "$ref": "#/components/schemas/Item" }, { "type": "array", "items": { - "$ref": "#/components/schemas/ItemModifier" + "$ref": "#/components/schemas/Item" } } ], - "title": "Response Itemmodifiers-Get All Item Modifiers" + "title": "Response Item-Get All Items" } } } @@ -883,12 +1247,10 @@ } }, "post": { - "tags": [ - "itemModifiers" - ], - "summary": "Create Item Modifier", - "description": "Create one or a list item modifiers.\n\nReturns the created item modifier or list of item modifiers.", - "operationId": "create_item_modifier", + "tags": ["item"], + "summary": "Create Item", + "description": "Create one or a list of new items.\n\nReturns the created item or list of items.", + "operationId": "create_item", "security": [ { "OAuth2PasswordBearer": [] @@ -919,16 +1281,16 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/ItemModifierCreate" + "$ref": "#/components/schemas/ItemCreate" }, { "type": "array", "items": { - "$ref": "#/components/schemas/ItemModifierCreate" + "$ref": "#/components/schemas/ItemCreate" } } ], - "title": "Itemmodifier" + "title": "Item" } } } @@ -941,19 +1303,19 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/ItemModifierCreate" + "$ref": "#/components/schemas/ItemCreate" }, { "type": "array", "items": { - "$ref": "#/components/schemas/ItemModifierCreate" + "$ref": "#/components/schemas/ItemCreate" } }, { "type": "null" } ], - "title": "Response Itemmodifiers-Create Item Modifier" + "title": "Response Item-Create Item" } } } @@ -971,55 +1333,112 @@ } } }, - "/api/api_v1/item/latest_item_id/": { + "/api/api_v1/league/{leagueId}": { "get": { - "tags": [ - "items", - "latest_item_id" + "tags": ["league"], + "summary": "Get League", + "description": "Get league by key and value for \"leagueId\".\n\nAlways returns one league.", + "operationId": "get_league", + "security": [ + { + "OAuth2PasswordBearer": [] + } + ], + "parameters": [ + { + "name": "leagueId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Leagueid" + } + } ], - "summary": "Get Latest Item Id", - "description": "Get the latest \"itemId\"\n\nCan only be used safely on an empty table or directly after an insertion.", - "operationId": "get_latest_item_id", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Response Items-Get Latest Item Id" + "$ref": "#/components/schemas/League" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" } } } } - }, + } + }, + "put": { + "tags": ["league"], + "summary": "Update Modifier", + "description": "Update a league by key and value for \"leagueId\"\n\nReturns the updated league.", + "operationId": "update_modifier", "security": [ { "OAuth2PasswordBearer": [] } - ] - } - }, - "/api/api_v1/item/": { - "get": { - "tags": [ - "items" ], - "summary": "Get All Items", - "description": "Get all items.\n\nReturns a list of all items.", - "operationId": "get_all_items", - "security": [ + "parameters": [ { - "OAuth2PasswordBearer": [] + "name": "leagueId", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Leagueid" + } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LeagueUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/League" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/api_v1/league/": { + "get": { + "tags": ["league"], + "summary": "Get All Leagues", + "description": "Get all leagues.\n\nReturns a list of all leagues.", + "operationId": "get_all_leagues", "parameters": [ { "name": "limit", @@ -1078,10 +1497,7 @@ "schema": { "anyOf": [ { - "enum": [ - "asc", - "desc" - ], + "enum": ["asc", "desc"], "type": "string" }, { @@ -1100,16 +1516,16 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/Item" + "$ref": "#/components/schemas/League" }, { "type": "array", "items": { - "$ref": "#/components/schemas/Item" + "$ref": "#/components/schemas/League" } } ], - "title": "Response Items-Get All Items" + "title": "Response League-Get All Leagues" } } } @@ -1127,12 +1543,10 @@ } }, "post": { - "tags": [ - "items" - ], - "summary": "Create Item", - "description": "Create one or a list of new items.\n\nReturns the created item or list of items.", - "operationId": "create_item", + "tags": ["league"], + "summary": "Create League", + "description": "Create one or a list of new leagues.\n\nReturns the created league or list of leagues.", + "operationId": "create_league", "security": [ { "OAuth2PasswordBearer": [] @@ -1163,16 +1577,16 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/ItemCreate" + "$ref": "#/components/schemas/LeagueCreate" }, { "type": "array", "items": { - "$ref": "#/components/schemas/ItemCreate" + "$ref": "#/components/schemas/LeagueCreate" } } ], - "title": "Item" + "title": "League" } } } @@ -1185,19 +1599,19 @@ "schema": { "anyOf": [ { - "$ref": "#/components/schemas/ItemCreate" + "$ref": "#/components/schemas/LeagueCreate" }, { "type": "array", "items": { - "$ref": "#/components/schemas/ItemCreate" + "$ref": "#/components/schemas/LeagueCreate" } }, { "type": "null" } ], - "title": "Response Items-Create Item" + "title": "Response League-Create League" } } } @@ -1215,12 +1629,38 @@ } } }, + "/api/api_v1/league/active_league/": { + "get": { + "tags": ["league"], + "summary": "Get Active Leagues", + "description": "Get leagues that are still valid/active\n\nAlways returns a list, but it may be empty.", + "operationId": "get_active_leagues", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/League" + }, + "type": "array", + "title": "Response League-Get Active Leagues" + } + } + } + } + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, "/api/api_v1/unidentifiedItem/latest_item_id/": { "get": { - "tags": [ - "unidentifiedItems", - "latest_item_id" - ], + "tags": ["unidentifiedItem", "latest_item_id"], "summary": "Get Latest Item Id", "description": "Get the latest \"itemId\"\n\nCan only be used safely on an empty table or directly after an insertion.", "operationId": "get_latest_item_id", @@ -1238,7 +1678,7 @@ "type": "null" } ], - "title": "Response Unidentifieditems-Get Latest Item Id" + "title": "Response Unidentifieditem-Get Latest Item Id" } } } @@ -1253,9 +1693,7 @@ }, "/api/api_v1/unidentifiedItem/": { "get": { - "tags": [ - "unidentifiedItems" - ], + "tags": ["unidentifiedItem"], "summary": "Get All Items", "description": "Get all items.\n\nReturns a list of all items.", "operationId": "get_all_items", @@ -1322,10 +1760,7 @@ "schema": { "anyOf": [ { - "enum": [ - "asc", - "desc" - ], + "enum": ["asc", "desc"], "type": "string" }, { @@ -1353,7 +1788,7 @@ } } ], - "title": "Response Unidentifieditems-Get All Items" + "title": "Response Unidentifieditem-Get All Items" } } } @@ -1371,9 +1806,7 @@ } }, "post": { - "tags": [ - "unidentifiedItems" - ], + "tags": ["unidentifiedItem"], "summary": "Create Item", "description": "Create one or a list of new items.\n\nReturns the created item or list of items.", "operationId": "create_item", @@ -1441,7 +1874,7 @@ "type": "null" } ], - "title": "Response Unidentifieditems-Create Item" + "title": "Response Unidentifieditem-Create Item" } } } @@ -1459,48 +1892,66 @@ } } }, - "/api/api_v1/modifier/{modifierId}": { + "/api/api_v1/unidentifiedItem/non_aggregated/": { "get": { - "tags": [ - "modifiers" - ], - "summary": "Get Modifier", - "description": "Get modifier or list of modifiers by key and\nvalue for \"modifierId\"\n\nDominant key is \"modifierId\".\n\nReturns one or a list of modifiers.", - "operationId": "get_modifier", + "tags": ["unidentifiedItem"], + "summary": "Get Non Aggregated", + "description": "Get the non aggregated unidentified items for the last 10 recorded hours\n (not necessarily the last 10 hours)", + "operationId": "get_non_aggregated", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/UnidentifiedItem" + }, + "type": "array", + "title": "Response Unidentifieditem-Get Non Aggregated" + } + } + } + } + }, "security": [ { "OAuth2PasswordBearer": [] } - ], - "parameters": [ - { - "name": "modifierId", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "title": "Modifierid" + ] + } + }, + "/api/api_v1/unidentifiedItem/add_aggregated/": { + "post": { + "tags": ["unidentifiedItem"], + "summary": "Add Aggregated", + "description": "Deletes the non aggregated unidentified items for the last 10 recorded hours\n (not necessarily the last 10 hours)\nAnd inserts the new items", + "operationId": "add_aggregated", + "requestBody": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/UnidentifiedItemCreate" + }, + "type": "array", + "title": "Aggregated Objs" + } } - } - ], + }, + "required": true + }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/Modifier" - }, - { - "type": "array", - "items": { - "$ref": "#/components/schemas/Modifier" - } - } - ], - "title": "Response Modifiers-Get Modifier" + "items": { + "$ref": "#/components/schemas/UnidentifiedItem" + }, + "type": "array", + "title": "Response Unidentifieditem-Add Aggregated" } } } @@ -1515,15 +1966,20 @@ } } } - } - }, - "delete": { - "tags": [ - "modifiers" - ], - "summary": "Delete Modifier", - "description": "Delete a modifier by key and value for \"modifierId\"\n\nReturns a message that the modifier was deleted.\nAlways deletes one modifier.", - "operationId": "delete_modifier", + }, + "security": [ + { + "OAuth2PasswordBearer": [] + } + ] + } + }, + "/api/api_v1/modifier/{modifierId}": { + "get": { + "tags": ["modifier"], + "summary": "Get Modifier", + "description": "Get modifier or list of modifiers by key and\nvalue for \"modifierId\"\n\nDominant key is \"modifierId\".\n\nReturns one or a list of modifiers.", + "operationId": "get_modifier", "security": [ { "OAuth2PasswordBearer": [] @@ -1538,15 +1994,6 @@ "type": "integer", "title": "Modifierid" } - }, - { - "name": "position", - "in": "query", - "required": true, - "schema": { - "type": "integer", - "title": "Position" - } } ], "responses": { @@ -1555,8 +2002,18 @@ "content": { "application/json": { "schema": { - "type": "string", - "title": "Response Modifiers-Delete Modifier" + "anyOf": [ + { + "$ref": "#/components/schemas/Modifier" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/Modifier" + } + } + ], + "title": "Response Modifier-Get Modifier" } } } @@ -1576,9 +2033,7 @@ }, "/api/api_v1/modifier/": { "get": { - "tags": [ - "modifiers" - ], + "tags": ["modifier"], "summary": "Get All Modifiers", "description": "Get all modifiers.\n\nReturns a list of all modifiers.", "operationId": "get_all_modifiers", @@ -1645,10 +2100,7 @@ "schema": { "anyOf": [ { - "enum": [ - "asc", - "desc" - ], + "enum": ["asc", "desc"], "type": "string" }, { @@ -1676,7 +2128,7 @@ } } ], - "title": "Response Modifiers-Get All Modifiers" + "title": "Response Modifier-Get All Modifiers" } } } @@ -1694,9 +2146,7 @@ } }, "post": { - "tags": [ - "modifiers" - ], + "tags": ["modifier"], "summary": "Create Modifier", "description": "Create one or a list of new modifiers.\n\nReturns the created modifier or list of modifiers.", "operationId": "create_modifier", @@ -1764,7 +2214,7 @@ "type": "null" } ], - "title": "Response Modifiers-Create Modifier" + "title": "Response Modifier-Create Modifier" } } } @@ -1782,11 +2232,9 @@ } }, "put": { - "tags": [ - "modifiers" - ], + "tags": ["modifier"], "summary": "Update Modifier", - "description": "Update a modifier by key and value for \"modifierId\"\n\nDominant key is \"modifierId\".\n\nReturns the updated modifier.", + "description": "Update a modifier by key and value for \"modifierId\" and \"position\"\n\nReturns the updated modifier.", "operationId": "update_modifier", "security": [ { @@ -1845,13 +2293,65 @@ } } } + }, + "delete": { + "tags": ["modifier"], + "summary": "Delete Modifier", + "description": "Delete a modifier by key and value for \"modifierId\"\n\nReturns a message that the modifier was deleted.\nAlways deletes one modifier.", + "operationId": "delete_modifier", + "security": [ + { + "OAuth2PasswordBearer": [] + } + ], + "parameters": [ + { + "name": "modifierId", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "title": "Modifierid" + } + }, + { + "name": "position", + "in": "query", + "required": true, + "schema": { + "type": "integer", + "title": "Position" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "string", + "title": "Response Modifier-Delete Modifier" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } } }, "/api/api_v1/modifier/grouped_modifiers_by_effect/": { "get": { - "tags": [ - "modifiers" - ], + "tags": ["modifier"], "summary": "Get Grouped Modifier By Effect", "description": "Get all grouped modifiers by effect.\n\nReturns a list of all grouped modifiers by effect.", "operationId": "get_grouped_modifier_by_effect", @@ -1872,7 +2372,7 @@ "type": "array" } ], - "title": "Response Modifiers-Get Grouped Modifier By Effect" + "title": "Response Modifier-Get Grouped Modifier By Effect" } } } @@ -1882,9 +2382,7 @@ }, "/api/api_v1/plot/": { "post": { - "tags": [ - "plots" - ], + "tags": ["plot"], "summary": "Get Plot Data", "description": "Takes a query based on the 'PlotQuery' schema and retrieves data\nto be used for plotting in the format of the 'PlotData' schema.\n\nThe 'PlotQuery' schema allows for modifier restriction and item specifications.", "operationId": "get_plot_data", @@ -1924,9 +2422,7 @@ }, "/api/api_v1/turnstile/": { "post": { - "tags": [ - "turnstiles" - ], + "tags": ["turnstile"], "summary": "Get Turnstile Validation", "description": "Takes a query based on the 'TurnstileQuery' schema and retrieves data\nbased on the cloudfare challenge turnstile validation response.\n\nArgs:\n query (schemas.TurnstileQuery): Query based on the 'TurnstileQuery' schema.\n verification (bool, optional): Verification flag. Defaults to Depends(verification).\n\nRaises:\n HTTPException: If verification fails.\n\nReturns:\n _type_: Returns a response based on the 'TurnstileResponse' schema.", "operationId": "get_turnstile_validation", @@ -1966,9 +2462,7 @@ }, "/api/api_v1/test/bulk-insert-test": { "post": { - "tags": [ - "tests" - ], + "tags": ["test"], "summary": "Bulk Insert Test", "description": "Can only be used in `settings.ENVIRONMENT=local` environment.\n\nTest route for bulk inserting records.\n\nReturns a success message once the insertion is complete.", "operationId": "bulk_insert_test", @@ -1996,7 +2490,7 @@ "schema": { "type": "object", "additionalProperties": true, - "title": "Response Tests-Bulk Insert Test" + "title": "Response Test-Bulk Insert Test" } } } @@ -2016,9 +2510,7 @@ }, "/api/api_v1/test/bulk-insert-users-and-verify": { "post": { - "tags": [ - "tests" - ], + "tags": ["test"], "summary": "Bulk Insert Users And Verify", "description": "Can only be used in `settings.ENVIRONMENT=local` environment.\n\nTest route for bulk inserting users and verifying them.\n\nReturns the access tokens for the created users.", "operationId": "bulk_insert_users_and_verify", @@ -2048,7 +2540,7 @@ "items": { "type": "string" }, - "title": "Response Tests-Bulk Insert Users And Verify" + "title": "Response Test-Bulk Insert Users And Verify" } } } @@ -2068,9 +2560,7 @@ }, "/api/api_v1/login/access-token": { "post": { - "tags": [ - "logins" - ], + "tags": ["login"], "summary": "Login Access Session", "description": "OAuth2 compatible session login.", "operationId": "login_access_session", @@ -2078,7 +2568,7 @@ "content": { "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/Body_logins-login_access_session" + "$ref": "#/components/schemas/Body_login-login_access_session" } } }, @@ -2150,7 +2640,7 @@ "type": "object", "title": "BaseSpecs" }, - "Body_logins-login_access_session": { + "Body_login-login_access_session": { "properties": { "grant_type": { "anyOf": [ @@ -2203,11 +2693,8 @@ } }, "type": "object", - "required": [ - "username", - "password" - ], - "title": "Body_logins-login_access_session" + "required": ["username", "password"], + "title": "Body_login-login_access_session" }, "Currency": { "properties": { @@ -2219,6 +2706,10 @@ "type": "number", "title": "Valueinchaos" }, + "leagueId": { + "type": "integer", + "title": "Leagueid" + }, "createdHoursSinceLaunch": { "type": "integer", "title": "Createdhourssincelaunch" @@ -2232,6 +2723,7 @@ "required": [ "tradeName", "valueInChaos", + "leagueId", "createdHoursSinceLaunch", "currencyId" ], @@ -2247,6 +2739,10 @@ "type": "number", "title": "Valueinchaos" }, + "leagueId": { + "type": "integer", + "title": "Leagueid" + }, "createdHoursSinceLaunch": { "type": "integer", "title": "Createdhourssincelaunch" @@ -2256,10 +2752,50 @@ "required": [ "tradeName", "valueInChaos", + "leagueId", "createdHoursSinceLaunch" ], "title": "CurrencyCreate" }, + "CurrencyQuery": { + "properties": { + "createdHoursSinceLaunch": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Createdhourssincelaunch" + }, + "tradeName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Tradename" + }, + "leagueId": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Leagueid" + } + }, + "type": "object", + "title": "CurrencyQuery" + }, "CurrencyUpdate": { "properties": { "tradeName": { @@ -2269,13 +2805,14 @@ "valueInChaos": { "type": "number", "title": "Valueinchaos" + }, + "leagueId": { + "type": "integer", + "title": "Leagueid" } }, "type": "object", - "required": [ - "tradeName", - "valueInChaos" - ], + "required": ["tradeName", "valueInChaos", "leagueId"], "title": "CurrencyUpdate" }, "Datum": { @@ -2294,11 +2831,7 @@ }, "confidence": { "type": "string", - "enum": [ - "low", - "medium", - "high" - ], + "enum": ["low", "medium", "high"], "title": "Confidence" } }, @@ -2387,10 +2920,7 @@ } }, "type": "object", - "required": [ - "position", - "textRolls" - ], + "required": ["position", "textRolls"], "title": "GroupedModifierProperties" }, "HTTPValidationError": { @@ -2491,9 +3021,9 @@ ], "title": "Name" }, - "league": { - "type": "string", - "title": "League" + "leagueId": { + "type": "integer", + "title": "Leagueid" }, "itemBaseTypeId": { "type": "integer", @@ -2676,7 +3206,7 @@ }, "type": "object", "required": [ - "league", + "leagueId", "itemBaseTypeId", "ilvl", "rarity", @@ -2723,11 +3253,7 @@ } }, "type": "object", - "required": [ - "baseType", - "category", - "itemBaseTypeId" - ], + "required": ["baseType", "category", "itemBaseTypeId"], "title": "ItemBaseType" }, "ItemBaseTypeCreate": { @@ -2764,10 +3290,7 @@ } }, "type": "object", - "required": [ - "baseType", - "category" - ], + "required": ["baseType", "category"], "title": "ItemBaseTypeCreate" }, "ItemBaseTypeUpdate": { @@ -2804,10 +3327,7 @@ } }, "type": "object", - "required": [ - "baseType", - "category" - ], + "required": ["baseType", "category"], "title": "ItemBaseTypeUpdate" }, "ItemCreate": { @@ -2823,9 +3343,9 @@ ], "title": "Name" }, - "league": { - "type": "string", - "title": "League" + "leagueId": { + "type": "integer", + "title": "Leagueid" }, "itemBaseTypeId": { "type": "integer", @@ -3004,7 +3524,7 @@ }, "type": "object", "required": [ - "league", + "leagueId", "itemBaseTypeId", "ilvl", "rarity", @@ -3249,6 +3769,110 @@ "type": "object", "title": "ItemSpecs" }, + "League": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "title": "Validfrom" + }, + "validTo": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Validto" + }, + "version": { + "type": "number", + "title": "Versionminor" + }, + "leagueId": { + "type": "integer", + "title": "Leagueid" + } + }, + "type": "object", + "required": ["name", "validFrom", "version", "leagueId"], + "title": "League" + }, + "LeagueCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "title": "Validfrom" + }, + "validTo": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Validto" + }, + "version": { + "type": "number", + "title": "Versionminor" + } + }, + "type": "object", + "required": ["name", "validFrom", "version"], + "title": "LeagueCreate" + }, + "LeagueUpdate": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "validFrom": { + "type": "string", + "format": "date-time", + "title": "Validfrom" + }, + "validTo": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Validto" + }, + "version": { + "type": "number", + "title": "Versionminor" + }, + "leagueId": { + "type": "integer", + "title": "Leagueid" + } + }, + "type": "object", + "required": ["name", "validFrom", "version", "leagueId"], + "title": "LeagueUpdate" + }, "MetadataObject": { "properties": { "interactive": { @@ -3464,12 +4088,7 @@ } }, "type": "object", - "required": [ - "position", - "effect", - "modifierId", - "createdAt" - ], + "required": ["position", "effect", "modifierId", "createdAt"], "title": "Modifier" }, "ModifierCreate": { @@ -3649,10 +4268,7 @@ } }, "type": "object", - "required": [ - "position", - "effect" - ], + "required": ["position", "effect"], "title": "ModifierCreate" }, "ModifierLimitation": { @@ -3696,9 +4312,7 @@ } }, "type": "object", - "required": [ - "position" - ], + "required": ["position"], "title": "ModifierLimitation" }, "ModifierUpdate": { @@ -3878,10 +4492,7 @@ } }, "type": "object", - "required": [ - "position", - "effect" - ], + "required": ["position", "effect"], "title": "ModifierUpdate" }, "PlotData": { @@ -3899,27 +4510,24 @@ } }, "type": "object", - "required": [ - "mostCommonCurrencyUsed", - "data" - ], + "required": ["mostCommonCurrencyUsed", "data"], "title": "PlotData" }, "PlotQuery": { "properties": { - "league": { + "leagueId": { "anyOf": [ { "items": { - "type": "string" + "type": "integer" }, "type": "array" }, { - "type": "string" + "type": "integer" } ], - "title": "League" + "title": "Leagueid" }, "itemSpecifications": { "anyOf": [ @@ -3979,17 +4587,15 @@ } }, "type": "object", - "required": [ - "league" - ], + "required": ["leagueId"], "title": "PlotQuery", "description": "Plots for items with or without modifiers" }, "TimeseriesData": { "properties": { - "name": { - "type": "string", - "title": "Name" + "leagueId": { + "type": "integer", + "title": "Leagueid" }, "data": { "items": { @@ -4000,20 +4606,12 @@ }, "confidenceRating": { "type": "string", - "enum": [ - "low", - "medium", - "high" - ], + "enum": ["low", "medium", "high"], "title": "Confidencerating" } }, "type": "object", - "required": [ - "name", - "data", - "confidenceRating" - ], + "required": ["leagueId", "data", "confidenceRating"], "title": "TimeseriesData" }, "Token": { @@ -4024,9 +4622,7 @@ } }, "type": "object", - "required": [ - "access_token" - ], + "required": ["access_token"], "title": "Token" }, "TurnstileQuery": { @@ -4041,10 +4637,7 @@ } }, "type": "object", - "required": [ - "token", - "ip" - ], + "required": ["token", "ip"], "title": "TurnstileQuery" }, "TurnstileResponse": { @@ -4123,9 +4716,7 @@ } }, "type": "object", - "required": [ - "success" - ], + "required": ["success"], "title": "TurnstileResponse" }, "UnidentifiedItem": { @@ -4141,9 +4732,9 @@ ], "title": "Name" }, - "league": { - "type": "string", - "title": "League" + "leagueId": { + "type": "integer", + "title": "Leagueid" }, "itemBaseTypeId": { "type": "integer", @@ -4184,6 +4775,16 @@ ], "title": "Currencyid" }, + "nItems": { + "type": "integer", + "title": "Nitems", + "default": 1 + }, + "aggregated": { + "type": "boolean", + "title": "Aggregated", + "default": false + }, "createdHoursSinceLaunch": { "type": "integer", "title": "Createdhourssincelaunch" @@ -4191,26 +4792,16 @@ "itemId": { "type": "integer", "title": "Itemid" - }, - "nItems": { - "type": "integer", - "title": "Nitems" - }, - "aggregated": { - "type": "boolean", - "title": "Aggregated" } }, "type": "object", "required": [ - "league", + "leagueId", "itemBaseTypeId", "ilvl", "rarity", "createdHoursSinceLaunch", - "itemId", - "nItems", - "aggregated" + "itemId" ], "title": "UnidentifiedItem" }, @@ -4227,9 +4818,9 @@ ], "title": "Name" }, - "league": { - "type": "string", - "title": "League" + "leagueId": { + "type": "integer", + "title": "Leagueid" }, "itemBaseTypeId": { "type": "integer", @@ -4270,6 +4861,16 @@ ], "title": "Currencyid" }, + "nItems": { + "type": "integer", + "title": "Nitems", + "default": 1 + }, + "aggregated": { + "type": "boolean", + "title": "Aggregated", + "default": false + }, "createdHoursSinceLaunch": { "type": "integer", "title": "Createdhourssincelaunch" @@ -4277,7 +4878,7 @@ }, "type": "object", "required": [ - "league", + "leagueId", "itemBaseTypeId", "ilvl", "rarity", @@ -4311,11 +4912,7 @@ } }, "type": "object", - "required": [ - "loc", - "msg", - "type" - ], + "required": ["loc", "msg", "type"], "title": "ValidationError" }, "WantedModifier": { @@ -4340,9 +4937,7 @@ } }, "type": "object", - "required": [ - "modifierId" - ], + "required": ["modifierId"], "title": "WantedModifier" } }, diff --git a/src/frontend/src/client/index.ts b/src/frontend/src/client/index.ts index 8ac0e314..54f96b3c 100644 --- a/src/frontend/src/client/index.ts +++ b/src/frontend/src/client/index.ts @@ -2,88 +2,99 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export { ApiError } from './core/ApiError'; -export { CancelablePromise, CancelError } from './core/CancelablePromise'; -export { OpenAPI } from './core/OpenAPI'; -export type { OpenAPIConfig } from './core/OpenAPI'; +export { ApiError } from "./core/ApiError"; +export { CancelablePromise, CancelError } from "./core/CancelablePromise"; +export { OpenAPI } from "./core/OpenAPI"; +export type { OpenAPIConfig } from "./core/OpenAPI"; -export type { BaseSpecs } from './models/BaseSpecs'; -export type { Body_logins_login_access_session } from './models/Body_logins_login_access_session'; -export type { Currency } from './models/Currency'; -export type { CurrencyCreate } from './models/CurrencyCreate'; -export type { CurrencyUpdate } from './models/CurrencyUpdate'; -export type { Datum } from './models/Datum'; -export type { GroupedModifierByEffect } from './models/GroupedModifierByEffect'; -export type { GroupedModifierProperties } from './models/GroupedModifierProperties'; -export type { HTTPValidationError } from './models/HTTPValidationError'; -export type { Influences } from './models/Influences'; -export type { Item } from './models/Item'; -export type { ItemBaseType } from './models/ItemBaseType'; -export type { ItemBaseTypeCreate } from './models/ItemBaseTypeCreate'; -export type { ItemBaseTypeUpdate } from './models/ItemBaseTypeUpdate'; -export type { ItemCreate } from './models/ItemCreate'; -export type { ItemModifier } from './models/ItemModifier'; -export type { ItemModifierCreate } from './models/ItemModifierCreate'; -export type { ItemSpecs } from './models/ItemSpecs'; -export type { MetadataObject } from './models/MetadataObject'; -export type { Modifier } from './models/Modifier'; -export type { ModifierCreate } from './models/ModifierCreate'; -export type { ModifierLimitation } from './models/ModifierLimitation'; -export type { ModifierUpdate } from './models/ModifierUpdate'; -export type { PlotData } from './models/PlotData'; -export type { PlotQuery } from './models/PlotQuery'; -export type { TimeseriesData } from './models/TimeseriesData'; -export type { Token } from './models/Token'; -export type { TurnstileQuery } from './models/TurnstileQuery'; -export type { TurnstileResponse } from './models/TurnstileResponse'; -export type { UnidentifiedItem } from './models/UnidentifiedItem'; -export type { UnidentifiedItemCreate } from './models/UnidentifiedItemCreate'; -export type { ValidationError } from './models/ValidationError'; -export type { WantedModifier } from './models/WantedModifier'; +export type { BaseSpecs } from "./models/BaseSpecs"; +export type { Body_login_login_access_session } from "./models/Body_login_login_access_session"; +export type { Currency } from "./models/Currency"; +export type { CurrencyCreate } from "./models/CurrencyCreate"; +export type { CurrencyQuery } from "./models/CurrencyQuery"; +export type { CurrencyUpdate } from "./models/CurrencyUpdate"; +export type { Datum } from "./models/Datum"; +export type { GroupedModifierByEffect } from "./models/GroupedModifierByEffect"; +export type { GroupedModifierProperties } from "./models/GroupedModifierProperties"; +export type { HTTPValidationError } from "./models/HTTPValidationError"; +export type { Influences } from "./models/Influences"; +export type { Item } from "./models/Item"; +export type { ItemBaseType } from "./models/ItemBaseType"; +export type { ItemBaseTypeCreate } from "./models/ItemBaseTypeCreate"; +export type { ItemBaseTypeUpdate } from "./models/ItemBaseTypeUpdate"; +export type { ItemCreate } from "./models/ItemCreate"; +export type { ItemModifier } from "./models/ItemModifier"; +export type { ItemModifierCreate } from "./models/ItemModifierCreate"; +export type { ItemSpecs } from "./models/ItemSpecs"; +export type { League } from "./models/League"; +export type { LeagueCreate } from "./models/LeagueCreate"; +export type { LeagueUpdate } from "./models/LeagueUpdate"; +export type { MetadataObject } from "./models/MetadataObject"; +export type { Modifier } from "./models/Modifier"; +export type { ModifierCreate } from "./models/ModifierCreate"; +export type { ModifierLimitation } from "./models/ModifierLimitation"; +export type { ModifierUpdate } from "./models/ModifierUpdate"; +export type { PlotData } from "./models/PlotData"; +export type { PlotQuery } from "./models/PlotQuery"; +export type { TimeseriesData } from "./models/TimeseriesData"; +export type { Token } from "./models/Token"; +export type { TurnstileQuery } from "./models/TurnstileQuery"; +export type { TurnstileResponse } from "./models/TurnstileResponse"; +export type { UnidentifiedItem } from "./models/UnidentifiedItem"; +export type { UnidentifiedItemCreate } from "./models/UnidentifiedItemCreate"; +export type { ValidationError } from "./models/ValidationError"; +export type { WantedModifier } from "./models/WantedModifier"; -export { $BaseSpecs } from './schemas/$BaseSpecs'; -export { $Body_logins_login_access_session } from './schemas/$Body_logins_login_access_session'; -export { $Currency } from './schemas/$Currency'; -export { $CurrencyCreate } from './schemas/$CurrencyCreate'; -export { $CurrencyUpdate } from './schemas/$CurrencyUpdate'; -export { $Datum } from './schemas/$Datum'; -export { $GroupedModifierByEffect } from './schemas/$GroupedModifierByEffect'; -export { $GroupedModifierProperties } from './schemas/$GroupedModifierProperties'; -export { $HTTPValidationError } from './schemas/$HTTPValidationError'; -export { $Influences } from './schemas/$Influences'; -export { $Item } from './schemas/$Item'; -export { $ItemBaseType } from './schemas/$ItemBaseType'; -export { $ItemBaseTypeCreate } from './schemas/$ItemBaseTypeCreate'; -export { $ItemBaseTypeUpdate } from './schemas/$ItemBaseTypeUpdate'; -export { $ItemCreate } from './schemas/$ItemCreate'; -export { $ItemModifier } from './schemas/$ItemModifier'; -export { $ItemModifierCreate } from './schemas/$ItemModifierCreate'; -export { $ItemSpecs } from './schemas/$ItemSpecs'; -export { $MetadataObject } from './schemas/$MetadataObject'; -export { $Modifier } from './schemas/$Modifier'; -export { $ModifierCreate } from './schemas/$ModifierCreate'; -export { $ModifierLimitation } from './schemas/$ModifierLimitation'; -export { $ModifierUpdate } from './schemas/$ModifierUpdate'; -export { $PlotData } from './schemas/$PlotData'; -export { $PlotQuery } from './schemas/$PlotQuery'; -export { $TimeseriesData } from './schemas/$TimeseriesData'; -export { $Token } from './schemas/$Token'; -export { $TurnstileQuery } from './schemas/$TurnstileQuery'; -export { $TurnstileResponse } from './schemas/$TurnstileResponse'; -export { $UnidentifiedItem } from './schemas/$UnidentifiedItem'; -export { $UnidentifiedItemCreate } from './schemas/$UnidentifiedItemCreate'; -export { $ValidationError } from './schemas/$ValidationError'; -export { $WantedModifier } from './schemas/$WantedModifier'; +export { $BaseSpecs } from "./schemas/$BaseSpecs"; +export { $Body_login_login_access_session } from "./schemas/$Body_login_login_access_session"; +export { $Currency } from "./schemas/$Currency"; +export { $CurrencyCreate } from "./schemas/$CurrencyCreate"; +export { $CurrencyQuery } from "./schemas/$CurrencyQuery"; +export { $CurrencyUpdate } from "./schemas/$CurrencyUpdate"; +export { $Datum } from "./schemas/$Datum"; +export { $GroupedModifierByEffect } from "./schemas/$GroupedModifierByEffect"; +export { $GroupedModifierProperties } from "./schemas/$GroupedModifierProperties"; +export { $HTTPValidationError } from "./schemas/$HTTPValidationError"; +export { $Influences } from "./schemas/$Influences"; +export { $Item } from "./schemas/$Item"; +export { $ItemBaseType } from "./schemas/$ItemBaseType"; +export { $ItemBaseTypeCreate } from "./schemas/$ItemBaseTypeCreate"; +export { $ItemBaseTypeUpdate } from "./schemas/$ItemBaseTypeUpdate"; +export { $ItemCreate } from "./schemas/$ItemCreate"; +export { $ItemModifier } from "./schemas/$ItemModifier"; +export { $ItemModifierCreate } from "./schemas/$ItemModifierCreate"; +export { $ItemSpecs } from "./schemas/$ItemSpecs"; +export { $League } from "./schemas/$League"; +export { $LeagueCreate } from "./schemas/$LeagueCreate"; +export { $LeagueUpdate } from "./schemas/$LeagueUpdate"; +export { $MetadataObject } from "./schemas/$MetadataObject"; +export { $Modifier } from "./schemas/$Modifier"; +export { $ModifierCreate } from "./schemas/$ModifierCreate"; +export { $ModifierLimitation } from "./schemas/$ModifierLimitation"; +export { $ModifierUpdate } from "./schemas/$ModifierUpdate"; +export { $PlotData } from "./schemas/$PlotData"; +export { $PlotQuery } from "./schemas/$PlotQuery"; +export { $TimeseriesData } from "./schemas/$TimeseriesData"; +export { $Token } from "./schemas/$Token"; +export { $TurnstileQuery } from "./schemas/$TurnstileQuery"; +export { $TurnstileResponse } from "./schemas/$TurnstileResponse"; +export { $UnidentifiedItem } from "./schemas/$UnidentifiedItem"; +export { $UnidentifiedItemCreate } from "./schemas/$UnidentifiedItemCreate"; +export { $ValidationError } from "./schemas/$ValidationError"; +export { $WantedModifier } from "./schemas/$WantedModifier"; -export { CurrencysService } from './services/CurrencysService'; -export { ItemBaseTypesService } from './services/ItemBaseTypesService'; -export { ItemModifiersService } from './services/ItemModifiersService'; -export { ItemsService } from './services/ItemsService'; -export { LatestCurrencyIdService } from './services/LatestCurrencyIdService'; -export { LatestItemIdService } from './services/LatestItemIdService'; -export { LoginsService } from './services/LoginsService'; -export { ModifiersService } from './services/ModifiersService'; -export { PlotsService } from './services/PlotsService'; -export { TestsService } from './services/TestsService'; -export { TurnstilesService } from './services/TurnstilesService'; -export { UnidentifiedItemsService } from './services/UnidentifiedItemsService'; +export { CurrencyService } from "./services/CurrencyService"; +export { ItemService } from "./services/ItemService"; +export { ItemBaseTypeService } from "./services/ItemBaseTypeService"; +export { ItemModifierService } from "./services/ItemModifierService"; +export { LatestCurrenciesService } from "./services/LatestCurrenciesService"; +export { LatestCurrencyIdService } from "./services/LatestCurrencyIdService"; +export { LatestHoursService } from "./services/LatestHoursService"; +export { LatestItemIdService } from "./services/LatestItemIdService"; +export { LeagueService } from "./services/LeagueService"; +export { LoginService } from "./services/LoginService"; +export { ModifierService } from "./services/ModifierService"; +export { PlotService } from "./services/PlotService"; +export { TestService } from "./services/TestService"; +export { TurnstileService } from "./services/TurnstileService"; +export { UnidentifiedItemService } from "./services/UnidentifiedItemService"; diff --git a/src/frontend/src/client/models/Body_logins_login_access_session.ts b/src/frontend/src/client/models/Body_login_login_access_session.ts similarity index 86% rename from src/frontend/src/client/models/Body_logins_login_access_session.ts rename to src/frontend/src/client/models/Body_login_login_access_session.ts index 24a927e1..28dedc0c 100644 --- a/src/frontend/src/client/models/Body_logins_login_access_session.ts +++ b/src/frontend/src/client/models/Body_login_login_access_session.ts @@ -2,7 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export type Body_logins_login_access_session = { +export type Body_login_login_access_session = { grant_type?: (string | null); username: string; password: string; diff --git a/src/frontend/src/client/models/Currency.ts b/src/frontend/src/client/models/Currency.ts index 2a8dbe85..3b317f29 100644 --- a/src/frontend/src/client/models/Currency.ts +++ b/src/frontend/src/client/models/Currency.ts @@ -5,6 +5,7 @@ export type Currency = { tradeName: string; valueInChaos: number; + leagueId: number; createdHoursSinceLaunch: number; currencyId: number; }; diff --git a/src/frontend/src/client/models/CurrencyCreate.ts b/src/frontend/src/client/models/CurrencyCreate.ts index 56807d6b..e9a852c8 100644 --- a/src/frontend/src/client/models/CurrencyCreate.ts +++ b/src/frontend/src/client/models/CurrencyCreate.ts @@ -5,6 +5,7 @@ export type CurrencyCreate = { tradeName: string; valueInChaos: number; + leagueId: number; createdHoursSinceLaunch: number; }; diff --git a/src/frontend/src/client/models/CurrencyQuery.ts b/src/frontend/src/client/models/CurrencyQuery.ts new file mode 100644 index 00000000..fcc3e538 --- /dev/null +++ b/src/frontend/src/client/models/CurrencyQuery.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type CurrencyQuery = { + createdHoursSinceLaunch?: (number | null); + tradeName?: (string | null); + leagueId?: (number | null); +}; + diff --git a/src/frontend/src/client/models/CurrencyUpdate.ts b/src/frontend/src/client/models/CurrencyUpdate.ts index 798b25ee..dff11882 100644 --- a/src/frontend/src/client/models/CurrencyUpdate.ts +++ b/src/frontend/src/client/models/CurrencyUpdate.ts @@ -5,5 +5,6 @@ export type CurrencyUpdate = { tradeName: string; valueInChaos: number; + leagueId: number; }; diff --git a/src/frontend/src/client/models/Item.ts b/src/frontend/src/client/models/Item.ts index f82c264e..b2fa4a8f 100644 --- a/src/frontend/src/client/models/Item.ts +++ b/src/frontend/src/client/models/Item.ts @@ -5,7 +5,7 @@ import type { Influences } from './Influences'; export type Item = { name?: (string | null); - league: string; + leagueId: number; itemBaseTypeId: number; ilvl: number; rarity: string; diff --git a/src/frontend/src/client/models/ItemCreate.ts b/src/frontend/src/client/models/ItemCreate.ts index e371bdcb..7dc3d527 100644 --- a/src/frontend/src/client/models/ItemCreate.ts +++ b/src/frontend/src/client/models/ItemCreate.ts @@ -5,7 +5,7 @@ import type { Influences } from './Influences'; export type ItemCreate = { name?: (string | null); - league: string; + leagueId: number; itemBaseTypeId: number; ilvl: number; rarity: string; diff --git a/src/frontend/src/client/models/League.ts b/src/frontend/src/client/models/League.ts new file mode 100644 index 00000000..d2af09e3 --- /dev/null +++ b/src/frontend/src/client/models/League.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type League = { + name: string; + validFrom: string; + validTo?: string | null; + version: number; + leagueId: number; +}; diff --git a/src/frontend/src/client/models/LeagueCreate.ts b/src/frontend/src/client/models/LeagueCreate.ts new file mode 100644 index 00000000..7a144614 --- /dev/null +++ b/src/frontend/src/client/models/LeagueCreate.ts @@ -0,0 +1,10 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type LeagueCreate = { + name: string; + validFrom: string; + validTo?: string | null; + version: number; +}; diff --git a/src/frontend/src/client/models/LeagueUpdate.ts b/src/frontend/src/client/models/LeagueUpdate.ts new file mode 100644 index 00000000..5bee70b4 --- /dev/null +++ b/src/frontend/src/client/models/LeagueUpdate.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type LeagueUpdate = { + name: string; + validFrom: string; + validTo?: string | null; + version: number; + leagueId: number; +}; diff --git a/src/frontend/src/client/models/PlotQuery.ts b/src/frontend/src/client/models/PlotQuery.ts index d00b12cf..51512638 100644 --- a/src/frontend/src/client/models/PlotQuery.ts +++ b/src/frontend/src/client/models/PlotQuery.ts @@ -9,7 +9,7 @@ import type { WantedModifier } from './WantedModifier'; * Plots for items with or without modifiers */ export type PlotQuery = { - league: (Array | string); + leagueId: (Array | number); itemSpecifications?: (ItemSpecs | null); baseSpecifications?: (BaseSpecs | null); end?: (number | null); diff --git a/src/frontend/src/client/models/TimeseriesData.ts b/src/frontend/src/client/models/TimeseriesData.ts index 08fff7a1..847a0b19 100644 --- a/src/frontend/src/client/models/TimeseriesData.ts +++ b/src/frontend/src/client/models/TimeseriesData.ts @@ -4,7 +4,7 @@ /* eslint-disable */ import type { Datum } from './Datum'; export type TimeseriesData = { - name: string; + leagueId: number; data: Array; confidenceRating: 'low' | 'medium' | 'high'; }; diff --git a/src/frontend/src/client/models/UnidentifiedItem.ts b/src/frontend/src/client/models/UnidentifiedItem.ts index 5fc7cee1..447a26c5 100644 --- a/src/frontend/src/client/models/UnidentifiedItem.ts +++ b/src/frontend/src/client/models/UnidentifiedItem.ts @@ -4,16 +4,16 @@ /* eslint-disable */ export type UnidentifiedItem = { name?: (string | null); - league: string; + leagueId: number; itemBaseTypeId: number; ilvl: number; rarity: string; identified?: boolean; currencyAmount?: (number | null); currencyId?: (number | null); + nItems?: number; + aggregated?: boolean; createdHoursSinceLaunch: number; itemId: number; - nItems: number; - aggregated: boolean; }; diff --git a/src/frontend/src/client/models/UnidentifiedItemCreate.ts b/src/frontend/src/client/models/UnidentifiedItemCreate.ts index 5a2a8125..7b4995f0 100644 --- a/src/frontend/src/client/models/UnidentifiedItemCreate.ts +++ b/src/frontend/src/client/models/UnidentifiedItemCreate.ts @@ -4,13 +4,15 @@ /* eslint-disable */ export type UnidentifiedItemCreate = { name?: (string | null); - league: string; + leagueId: number; itemBaseTypeId: number; ilvl: number; rarity: string; identified?: boolean; currencyAmount?: (number | null); currencyId?: (number | null); + nItems?: number; + aggregated?: boolean; createdHoursSinceLaunch: number; }; diff --git a/src/frontend/src/client/schemas/$Body_logins_login_access_session.ts b/src/frontend/src/client/schemas/$Body_login_login_access_session.ts similarity index 95% rename from src/frontend/src/client/schemas/$Body_logins_login_access_session.ts rename to src/frontend/src/client/schemas/$Body_login_login_access_session.ts index 57daaa86..d8b8a4ef 100644 --- a/src/frontend/src/client/schemas/$Body_logins_login_access_session.ts +++ b/src/frontend/src/client/schemas/$Body_login_login_access_session.ts @@ -2,7 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export const $Body_logins_login_access_session = { +export const $Body_login_login_access_session = { properties: { grant_type: { type: 'any-of', diff --git a/src/frontend/src/client/schemas/$Currency.ts b/src/frontend/src/client/schemas/$Currency.ts index f65be219..1ec0e51c 100644 --- a/src/frontend/src/client/schemas/$Currency.ts +++ b/src/frontend/src/client/schemas/$Currency.ts @@ -12,6 +12,10 @@ export const $Currency = { type: 'number', isRequired: true, }, + leagueId: { + type: 'number', + isRequired: true, + }, createdHoursSinceLaunch: { type: 'number', isRequired: true, diff --git a/src/frontend/src/client/schemas/$CurrencyCreate.ts b/src/frontend/src/client/schemas/$CurrencyCreate.ts index 815ece5d..07dc7433 100644 --- a/src/frontend/src/client/schemas/$CurrencyCreate.ts +++ b/src/frontend/src/client/schemas/$CurrencyCreate.ts @@ -12,6 +12,10 @@ export const $CurrencyCreate = { type: 'number', isRequired: true, }, + leagueId: { + type: 'number', + isRequired: true, + }, createdHoursSinceLaunch: { type: 'number', isRequired: true, diff --git a/src/frontend/src/client/schemas/$CurrencyQuery.ts b/src/frontend/src/client/schemas/$CurrencyQuery.ts new file mode 100644 index 00000000..8584c151 --- /dev/null +++ b/src/frontend/src/client/schemas/$CurrencyQuery.ts @@ -0,0 +1,32 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $CurrencyQuery = { + properties: { + createdHoursSinceLaunch: { + type: 'any-of', + contains: [{ + type: 'number', + }, { + type: 'null', + }], + }, + tradeName: { + type: 'any-of', + contains: [{ + type: 'string', + }, { + type: 'null', + }], + }, + leagueId: { + type: 'any-of', + contains: [{ + type: 'number', + }, { + type: 'null', + }], + }, + }, +} as const; diff --git a/src/frontend/src/client/schemas/$CurrencyUpdate.ts b/src/frontend/src/client/schemas/$CurrencyUpdate.ts index a757d8a7..c98f26a6 100644 --- a/src/frontend/src/client/schemas/$CurrencyUpdate.ts +++ b/src/frontend/src/client/schemas/$CurrencyUpdate.ts @@ -12,5 +12,9 @@ export const $CurrencyUpdate = { type: 'number', isRequired: true, }, + leagueId: { + type: 'number', + isRequired: true, + }, }, } as const; diff --git a/src/frontend/src/client/schemas/$Item.ts b/src/frontend/src/client/schemas/$Item.ts index 6beee37e..ec1728b0 100644 --- a/src/frontend/src/client/schemas/$Item.ts +++ b/src/frontend/src/client/schemas/$Item.ts @@ -12,8 +12,8 @@ export const $Item = { type: 'null', }], }, - league: { - type: 'string', + leagueId: { + type: 'number', isRequired: true, }, itemBaseTypeId: { diff --git a/src/frontend/src/client/schemas/$ItemCreate.ts b/src/frontend/src/client/schemas/$ItemCreate.ts index 1995e1f3..5044d1e3 100644 --- a/src/frontend/src/client/schemas/$ItemCreate.ts +++ b/src/frontend/src/client/schemas/$ItemCreate.ts @@ -12,8 +12,8 @@ export const $ItemCreate = { type: 'null', }], }, - league: { - type: 'string', + leagueId: { + type: 'number', isRequired: true, }, itemBaseTypeId: { diff --git a/src/frontend/src/client/schemas/$League.ts b/src/frontend/src/client/schemas/$League.ts new file mode 100644 index 00000000..b5c8e700 --- /dev/null +++ b/src/frontend/src/client/schemas/$League.ts @@ -0,0 +1,37 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $League = { + properties: { + name: { + type: "string", + isRequired: true, + }, + validFrom: { + type: "string", + isRequired: true, + format: "date-time", + }, + validTo: { + type: "any-of", + contains: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + }, + version: { + type: "number", + isRequired: true, + }, + leagueId: { + type: "number", + isRequired: true, + }, + }, +} as const; diff --git a/src/frontend/src/client/schemas/$LeagueCreate.ts b/src/frontend/src/client/schemas/$LeagueCreate.ts new file mode 100644 index 00000000..abd09a3e --- /dev/null +++ b/src/frontend/src/client/schemas/$LeagueCreate.ts @@ -0,0 +1,33 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LeagueCreate = { + properties: { + name: { + type: "string", + isRequired: true, + }, + validFrom: { + type: "string", + isRequired: true, + format: "date-time", + }, + validTo: { + type: "any-of", + contains: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + }, + version: { + type: "number", + isRequired: true, + }, + }, +} as const; diff --git a/src/frontend/src/client/schemas/$LeagueUpdate.ts b/src/frontend/src/client/schemas/$LeagueUpdate.ts new file mode 100644 index 00000000..ed324181 --- /dev/null +++ b/src/frontend/src/client/schemas/$LeagueUpdate.ts @@ -0,0 +1,37 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export const $LeagueUpdate = { + properties: { + name: { + type: "string", + isRequired: true, + }, + validFrom: { + type: "string", + isRequired: true, + format: "date-time", + }, + validTo: { + type: "any-of", + contains: [ + { + type: "string", + format: "date-time", + }, + { + type: "null", + }, + ], + }, + version: { + type: "number", + isRequired: true, + }, + leagueId: { + type: "number", + isRequired: true, + }, + }, +} as const; diff --git a/src/frontend/src/client/schemas/$PlotQuery.ts b/src/frontend/src/client/schemas/$PlotQuery.ts index 502d1d03..ef30c8bb 100644 --- a/src/frontend/src/client/schemas/$PlotQuery.ts +++ b/src/frontend/src/client/schemas/$PlotQuery.ts @@ -5,15 +5,15 @@ export const $PlotQuery = { description: `Plots for items with or without modifiers`, properties: { - league: { + leagueId: { type: 'any-of', contains: [{ type: 'array', contains: { - type: 'string', + type: 'number', }, }, { - type: 'string', + type: 'number', }], isRequired: true, }, diff --git a/src/frontend/src/client/schemas/$TimeseriesData.ts b/src/frontend/src/client/schemas/$TimeseriesData.ts index dc3e0eb8..fbcbef95 100644 --- a/src/frontend/src/client/schemas/$TimeseriesData.ts +++ b/src/frontend/src/client/schemas/$TimeseriesData.ts @@ -4,8 +4,8 @@ /* eslint-disable */ export const $TimeseriesData = { properties: { - name: { - type: 'string', + leagueId: { + type: 'number', isRequired: true, }, data: { diff --git a/src/frontend/src/client/schemas/$UnidentifiedItem.ts b/src/frontend/src/client/schemas/$UnidentifiedItem.ts index f1be8b31..944323d9 100644 --- a/src/frontend/src/client/schemas/$UnidentifiedItem.ts +++ b/src/frontend/src/client/schemas/$UnidentifiedItem.ts @@ -12,8 +12,8 @@ export const $UnidentifiedItem = { type: 'null', }], }, - league: { - type: 'string', + leagueId: { + type: 'number', isRequired: true, }, itemBaseTypeId: { @@ -47,20 +47,18 @@ export const $UnidentifiedItem = { type: 'null', }], }, - createdHoursSinceLaunch: { + nItems: { type: 'number', - isRequired: true, }, - itemId: { - type: 'number', - isRequired: true, + aggregated: { + type: 'boolean', }, - nItems: { + createdHoursSinceLaunch: { type: 'number', isRequired: true, }, - aggregated: { - type: 'boolean', + itemId: { + type: 'number', isRequired: true, }, }, diff --git a/src/frontend/src/client/schemas/$UnidentifiedItemCreate.ts b/src/frontend/src/client/schemas/$UnidentifiedItemCreate.ts index 0099c08b..9a15f3ea 100644 --- a/src/frontend/src/client/schemas/$UnidentifiedItemCreate.ts +++ b/src/frontend/src/client/schemas/$UnidentifiedItemCreate.ts @@ -12,8 +12,8 @@ export const $UnidentifiedItemCreate = { type: 'null', }], }, - league: { - type: 'string', + leagueId: { + type: 'number', isRequired: true, }, itemBaseTypeId: { @@ -47,6 +47,12 @@ export const $UnidentifiedItemCreate = { type: 'null', }], }, + nItems: { + type: 'number', + }, + aggregated: { + type: 'boolean', + }, createdHoursSinceLaunch: { type: 'number', isRequired: true, diff --git a/src/frontend/src/client/services/CurrencysService.ts b/src/frontend/src/client/services/CurrencyService.ts similarity index 65% rename from src/frontend/src/client/services/CurrencysService.ts rename to src/frontend/src/client/services/CurrencyService.ts index 067a5409..53938b96 100644 --- a/src/frontend/src/client/services/CurrencysService.ts +++ b/src/frontend/src/client/services/CurrencyService.ts @@ -4,11 +4,12 @@ /* eslint-disable */ import type { Currency } from '../models/Currency'; import type { CurrencyCreate } from '../models/CurrencyCreate'; +import type { CurrencyQuery } from '../models/CurrencyQuery'; import type { CurrencyUpdate } from '../models/CurrencyUpdate'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; -export class CurrencysService { +export class CurrencyService { /** * Get Currency * Get currency by key and value for "currencyId". @@ -33,6 +34,67 @@ export class CurrencysService { }, }); } + /** + * Get All Currencies + * Get all currencies. + * + * Returns a list of all currencies. + * @returns any Successful Response + * @throws ApiError + */ + public static getAllCurrencies({ + limit, + skip, + sortKey, + sortMethod, + }: { + limit?: (number | null), + skip?: (number | null), + sortKey?: (string | null), + sortMethod?: ('asc' | 'desc' | null), + }): CancelablePromise<(Currency | Array)> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/api_v1/currency/', + query: { + 'limit': limit, + 'skip': skip, + 'sort_key': sortKey, + 'sort_method': sortMethod, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Create Currency + * Create one or a list of currencies. + * + * Returns the created currency or list of currencies. + * @returns any Successful Response + * @throws ApiError + */ + public static createCurrency({ + requestBody, + returnNothing, + }: { + requestBody: (CurrencyCreate | Array), + returnNothing?: (boolean | null), + }): CancelablePromise<(CurrencyCreate | Array | null)> { + return __request(OpenAPI, { + method: 'POST', + url: '/api/api_v1/currency/', + query: { + 'return_nothing': returnNothing, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } /** * Update Currency * Update a currency by key and value for "currencyId". @@ -50,8 +112,8 @@ export class CurrencysService { }): CancelablePromise { return __request(OpenAPI, { method: 'PUT', - url: '/api/api_v1/currency/{currencyId}', - path: { + url: '/api/api_v1/currency/', + query: { 'currencyId': currencyId, }, body: requestBody, @@ -77,8 +139,8 @@ export class CurrencysService { }): CancelablePromise { return __request(OpenAPI, { method: 'DELETE', - url: '/api/api_v1/currency/{currencyId}', - path: { + url: '/api/api_v1/currency/', + query: { 'currencyId': currencyId, }, errors: { @@ -87,32 +149,37 @@ export class CurrencysService { }); } /** - * Get All Currencies - * Get all currencies. + * Get Latest Currency Id + * Get the latest currencyId, returns 1 if table is empty * - * Returns a list of all currencies. - * @returns any Successful Response + * Can only be used safely on an empty table or directly after an insertion. + * @returns number Successful Response * @throws ApiError */ - public static getAllCurrencies({ - limit, - skip, - sortKey, - sortMethod, + public static getLatestCurrencyId(): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/api_v1/currency/latest_currency_id/', + }); + } + /** + * Get Latest Hours + * Returns a dict mapping league id to the most recent hour relating to that league. Dict is empty if no currencies exist. + * + * Does not guarantee an entry for every league id. + * @returns number Successful Response + * @throws ApiError + */ + public static getLatestHours({ + leagueIds, }: { - limit?: (number | null), - skip?: (number | null), - sortKey?: (string | null), - sortMethod?: ('asc' | 'desc' | null), - }): CancelablePromise<(Currency | Array)> { + leagueIds: Array, + }): CancelablePromise> { return __request(OpenAPI, { method: 'GET', - url: '/api/api_v1/currency/', + url: '/api/api_v1/currency/latest_hours/', query: { - 'limit': limit, - 'skip': skip, - 'sort_key': sortKey, - 'sort_method': sortMethod, + 'league_ids': leagueIds, }, errors: { 422: `Validation Error`, @@ -120,45 +187,48 @@ export class CurrencysService { }); } /** - * Create Currency - * Create one or a list of currencies. + * Get Latest Currencies + * Returns a dict mapping league id to most recent currencies relating to that league. Dict is empty if no currencies exist. * - * Returns the created currency or list of currencies. - * @returns any Successful Response + * Does not guarantee an entry for every league id. + * @returns Currency Successful Response * @throws ApiError */ - public static createCurrency({ - requestBody, - returnNothing, + public static getLatestCurrencies({ + leagueIds, }: { - requestBody: (CurrencyCreate | Array), - returnNothing?: (boolean | null), - }): CancelablePromise<(CurrencyCreate | Array | null)> { + leagueIds: Array, + }): CancelablePromise>> { return __request(OpenAPI, { - method: 'POST', - url: '/api/api_v1/currency/', + method: 'GET', + url: '/api/api_v1/currency/latest_currencies/', query: { - 'return_nothing': returnNothing, + 'league_ids': leagueIds, }, - body: requestBody, - mediaType: 'application/json', errors: { 422: `Validation Error`, }, }); } /** - * Get Latest Currency Id - * Get the latest currencyId - * - * Can only be used safely on an empty table or directly after an insertion. - * @returns number Successful Response + * Get Currency From Query + * Returns a list of currencies that match any of the queries + * @returns Currency Successful Response * @throws ApiError */ - public static getLatestCurrencyId(): CancelablePromise { + public static getCurrencyFromQuery({ + requestBody, + }: { + requestBody: Array, + }): CancelablePromise> { return __request(OpenAPI, { - method: 'GET', - url: '/api/api_v1/currency/latest_currency_id/', + method: 'POST', + url: '/api/api_v1/currency/from_query/', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, }); } } diff --git a/src/frontend/src/client/services/ItemBaseTypesService.ts b/src/frontend/src/client/services/ItemBaseTypeService.ts similarity index 95% rename from src/frontend/src/client/services/ItemBaseTypesService.ts rename to src/frontend/src/client/services/ItemBaseTypeService.ts index 18e05dda..320dfaa9 100644 --- a/src/frontend/src/client/services/ItemBaseTypesService.ts +++ b/src/frontend/src/client/services/ItemBaseTypeService.ts @@ -8,7 +8,7 @@ import type { ItemBaseTypeUpdate } from '../models/ItemBaseTypeUpdate'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; -export class ItemBaseTypesService { +export class ItemBaseTypeService { /** * Get Item Base Type * Get item base type by key and value for "itemBaseTypeId". @@ -33,59 +33,6 @@ export class ItemBaseTypesService { }, }); } - /** - * Update Item Base Type - * Update an item base type by key and value for "itemBaseTypeId". - * - * Returns the updated item base type. - * @returns ItemBaseType Successful Response - * @throws ApiError - */ - public static updateItemBaseType({ - itemBaseTypeId, - requestBody, - }: { - itemBaseTypeId: number, - requestBody: ItemBaseTypeUpdate, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'PUT', - url: '/api/api_v1/itemBaseType/{itemBaseTypeId}', - path: { - 'itemBaseTypeId': itemBaseTypeId, - }, - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * Delete Item Base Type - * Delete an item base type by key and value for "itemBaseTypeId". - * - * Returns a message that the item base type was deleted successfully. - * Always deletes one item base type. - * @returns string Successful Response - * @throws ApiError - */ - public static deleteItemBaseType({ - itemBaseTypeId, - }: { - itemBaseTypeId: number, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'DELETE', - url: '/api/api_v1/itemBaseType/{itemBaseTypeId}', - path: { - 'itemBaseTypeId': itemBaseTypeId, - }, - errors: { - 422: `Validation Error`, - }, - }); - } /** * Get All Item Base Types * Get all item base types. @@ -150,4 +97,57 @@ export class ItemBaseTypesService { }, }); } + /** + * Update Item Base Type + * Update an item base type by key and value for "itemBaseTypeId". + * + * Returns the updated item base type. + * @returns ItemBaseType Successful Response + * @throws ApiError + */ + public static updateItemBaseType({ + itemBaseTypeId, + requestBody, + }: { + itemBaseTypeId: number, + requestBody: ItemBaseTypeUpdate, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'PUT', + url: '/api/api_v1/itemBaseType/', + query: { + 'itemBaseTypeId': itemBaseTypeId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Delete Item Base Type + * Delete an item base type by key and value for "itemBaseTypeId". + * + * Returns a message that the item base type was deleted successfully. + * Always deletes one item base type. + * @returns string Successful Response + * @throws ApiError + */ + public static deleteItemBaseType({ + itemBaseTypeId, + }: { + itemBaseTypeId: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/api/api_v1/itemBaseType/', + query: { + 'itemBaseTypeId': itemBaseTypeId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } } diff --git a/src/frontend/src/client/services/ItemModifiersService.ts b/src/frontend/src/client/services/ItemModifierService.ts similarity index 98% rename from src/frontend/src/client/services/ItemModifiersService.ts rename to src/frontend/src/client/services/ItemModifierService.ts index 5468dc76..bad76ffe 100644 --- a/src/frontend/src/client/services/ItemModifiersService.ts +++ b/src/frontend/src/client/services/ItemModifierService.ts @@ -7,7 +7,7 @@ import type { ItemModifierCreate } from '../models/ItemModifierCreate'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; -export class ItemModifiersService { +export class ItemModifierService { /** * Get All Item Modifiers * Get all item modifiers. diff --git a/src/frontend/src/client/services/ItemsService.ts b/src/frontend/src/client/services/ItemService.ts similarity index 98% rename from src/frontend/src/client/services/ItemsService.ts rename to src/frontend/src/client/services/ItemService.ts index 0c10cf6a..1bf7d448 100644 --- a/src/frontend/src/client/services/ItemsService.ts +++ b/src/frontend/src/client/services/ItemService.ts @@ -7,7 +7,7 @@ import type { ItemCreate } from '../models/ItemCreate'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; -export class ItemsService { +export class ItemService { /** * Get Latest Item Id * Get the latest "itemId" diff --git a/src/frontend/src/client/services/LatestCurrenciesService.ts b/src/frontend/src/client/services/LatestCurrenciesService.ts new file mode 100644 index 00000000..690aee2d --- /dev/null +++ b/src/frontend/src/client/services/LatestCurrenciesService.ts @@ -0,0 +1,34 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { Currency } from '../models/Currency'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class LatestCurrenciesService { + /** + * Get Latest Currencies + * Returns a dict mapping league id to most recent currencies relating to that league. Dict is empty if no currencies exist. + * + * Does not guarantee an entry for every league id. + * @returns Currency Successful Response + * @throws ApiError + */ + public static getLatestCurrencies({ + leagueIds, + }: { + leagueIds: Array, + }): CancelablePromise>> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/api_v1/currency/latest_currencies/', + query: { + 'league_ids': leagueIds, + }, + errors: { + 422: `Validation Error`, + }, + }); + } +} diff --git a/src/frontend/src/client/services/LatestCurrencyIdService.ts b/src/frontend/src/client/services/LatestCurrencyIdService.ts index 5df88f8d..b46df99c 100644 --- a/src/frontend/src/client/services/LatestCurrencyIdService.ts +++ b/src/frontend/src/client/services/LatestCurrencyIdService.ts @@ -8,7 +8,7 @@ import { request as __request } from '../core/request'; export class LatestCurrencyIdService { /** * Get Latest Currency Id - * Get the latest currencyId + * Get the latest currencyId, returns 1 if table is empty * * Can only be used safely on an empty table or directly after an insertion. * @returns number Successful Response diff --git a/src/frontend/src/client/services/LatestHoursService.ts b/src/frontend/src/client/services/LatestHoursService.ts new file mode 100644 index 00000000..76b5753b --- /dev/null +++ b/src/frontend/src/client/services/LatestHoursService.ts @@ -0,0 +1,33 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class LatestHoursService { + /** + * Get Latest Hours + * Returns a dict mapping league id to the most recent hour relating to that league. Dict is empty if no currencies exist. + * + * Does not guarantee an entry for every league id. + * @returns number Successful Response + * @throws ApiError + */ + public static getLatestHours({ + leagueIds, + }: { + leagueIds: Array, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/api_v1/currency/latest_hours/', + query: { + 'league_ids': leagueIds, + }, + errors: { + 422: `Validation Error`, + }, + }); + } +} diff --git a/src/frontend/src/client/services/LeagueService.ts b/src/frontend/src/client/services/LeagueService.ts new file mode 100644 index 00000000..8812b05f --- /dev/null +++ b/src/frontend/src/client/services/LeagueService.ts @@ -0,0 +1,139 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { League } from '../models/League'; +import type { LeagueCreate } from '../models/LeagueCreate'; +import type { LeagueUpdate } from '../models/LeagueUpdate'; +import type { CancelablePromise } from '../core/CancelablePromise'; +import { OpenAPI } from '../core/OpenAPI'; +import { request as __request } from '../core/request'; +export class LeagueService { + /** + * Get League + * Get league by key and value for "leagueId". + * + * Always returns one league. + * @returns League Successful Response + * @throws ApiError + */ + public static getLeague({ + leagueId, + }: { + leagueId: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/api/api_v1/league/{leagueId}', + path: { + 'leagueId': leagueId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Update Modifier + * Update a league by key and value for "leagueId" + * + * Returns the updated league. + * @returns League Successful Response + * @throws ApiError + */ + public static updateModifier({ + leagueId, + requestBody, + }: { + leagueId: number, + requestBody: LeagueUpdate, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'PUT', + url: '/api/api_v1/league/{leagueId}', + path: { + 'leagueId': leagueId, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get All Leagues + * Get all leagues. + * + * Returns a list of all leagues. + * @returns any Successful Response + * @throws ApiError + */ + public static getAllLeagues({ + limit, + skip, + sortKey, + sortMethod, + }: { + limit?: (number | null), + skip?: (number | null), + sortKey?: (string | null), + sortMethod?: ('asc' | 'desc' | null), + }): CancelablePromise<(League | Array)> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/api_v1/league/', + query: { + 'limit': limit, + 'skip': skip, + 'sort_key': sortKey, + 'sort_method': sortMethod, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Create League + * Create one or a list of new leagues. + * + * Returns the created league or list of leagues. + * @returns any Successful Response + * @throws ApiError + */ + public static createLeague({ + requestBody, + returnNothing, + }: { + requestBody: (LeagueCreate | Array), + returnNothing?: (boolean | null), + }): CancelablePromise<(LeagueCreate | Array | null)> { + return __request(OpenAPI, { + method: 'POST', + url: '/api/api_v1/league/', + query: { + 'return_nothing': returnNothing, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } + /** + * Get Active Leagues + * Get leagues that are still valid/active + * + * Always returns a list, but it may be empty. + * @returns League Successful Response + * @throws ApiError + */ + public static getActiveLeagues(): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/api_v1/league/active_league/', + }); + } +} diff --git a/src/frontend/src/client/services/LoginsService.ts b/src/frontend/src/client/services/LoginService.ts similarity index 83% rename from src/frontend/src/client/services/LoginsService.ts rename to src/frontend/src/client/services/LoginService.ts index ebdaca76..1277e64b 100644 --- a/src/frontend/src/client/services/LoginsService.ts +++ b/src/frontend/src/client/services/LoginService.ts @@ -2,12 +2,12 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { Body_logins_login_access_session } from '../models/Body_logins_login_access_session'; +import type { Body_login_login_access_session } from '../models/Body_login_login_access_session'; import type { Token } from '../models/Token'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; -export class LoginsService { +export class LoginService { /** * Login Access Session * OAuth2 compatible session login. @@ -17,7 +17,7 @@ export class LoginsService { public static loginAccessSession({ formData, }: { - formData: Body_logins_login_access_session, + formData: Body_login_login_access_session, }): CancelablePromise { return __request(OpenAPI, { method: 'POST', diff --git a/src/frontend/src/client/services/ModifiersService.ts b/src/frontend/src/client/services/ModifierService.ts similarity index 95% rename from src/frontend/src/client/services/ModifiersService.ts rename to src/frontend/src/client/services/ModifierService.ts index 840e2ada..90b8b318 100644 --- a/src/frontend/src/client/services/ModifiersService.ts +++ b/src/frontend/src/client/services/ModifierService.ts @@ -9,7 +9,7 @@ import type { ModifierUpdate } from '../models/ModifierUpdate'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; -export class ModifiersService { +export class ModifierService { /** * Get Modifier * Get modifier or list of modifiers by key and @@ -37,36 +37,6 @@ export class ModifiersService { }, }); } - /** - * Delete Modifier - * Delete a modifier by key and value for "modifierId" - * - * Returns a message that the modifier was deleted. - * Always deletes one modifier. - * @returns string Successful Response - * @throws ApiError - */ - public static deleteModifier({ - modifierId, - position, - }: { - modifierId: number, - position: number, - }): CancelablePromise { - return __request(OpenAPI, { - method: 'DELETE', - url: '/api/api_v1/modifier/{modifierId}', - path: { - 'modifierId': modifierId, - }, - query: { - 'position': position, - }, - errors: { - 422: `Validation Error`, - }, - }); - } /** * Get All Modifiers * Get all modifiers. @@ -130,9 +100,7 @@ export class ModifiersService { } /** * Update Modifier - * Update a modifier by key and value for "modifierId" - * - * Dominant key is "modifierId". + * Update a modifier by key and value for "modifierId" and "position" * * Returns the updated modifier. * @returns Modifier Successful Response @@ -161,6 +129,34 @@ export class ModifiersService { }, }); } + /** + * Delete Modifier + * Delete a modifier by key and value for "modifierId" + * + * Returns a message that the modifier was deleted. + * Always deletes one modifier. + * @returns string Successful Response + * @throws ApiError + */ + public static deleteModifier({ + modifierId, + position, + }: { + modifierId: number, + position: number, + }): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/api/api_v1/modifier/', + query: { + 'modifierId': modifierId, + 'position': position, + }, + errors: { + 422: `Validation Error`, + }, + }); + } /** * Get Grouped Modifier By Effect * Get all grouped modifiers by effect. diff --git a/src/frontend/src/client/services/PlotsService.ts b/src/frontend/src/client/services/PlotService.ts similarity index 97% rename from src/frontend/src/client/services/PlotsService.ts rename to src/frontend/src/client/services/PlotService.ts index bfc21dcd..b6d77425 100644 --- a/src/frontend/src/client/services/PlotsService.ts +++ b/src/frontend/src/client/services/PlotService.ts @@ -7,7 +7,7 @@ import type { PlotQuery } from '../models/PlotQuery'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; -export class PlotsService { +export class PlotService { /** * Get Plot Data * Takes a query based on the 'PlotQuery' schema and retrieves data diff --git a/src/frontend/src/client/services/TestsService.ts b/src/frontend/src/client/services/TestService.ts similarity index 98% rename from src/frontend/src/client/services/TestsService.ts rename to src/frontend/src/client/services/TestService.ts index 8f868698..4d9bba7b 100644 --- a/src/frontend/src/client/services/TestsService.ts +++ b/src/frontend/src/client/services/TestService.ts @@ -5,7 +5,7 @@ import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; -export class TestsService { +export class TestService { /** * Bulk Insert Test * Can only be used in `settings.ENVIRONMENT=local` environment. diff --git a/src/frontend/src/client/services/TurnstilesService.ts b/src/frontend/src/client/services/TurnstileService.ts similarity index 97% rename from src/frontend/src/client/services/TurnstilesService.ts rename to src/frontend/src/client/services/TurnstileService.ts index 7ab9afc5..a520df9c 100644 --- a/src/frontend/src/client/services/TurnstilesService.ts +++ b/src/frontend/src/client/services/TurnstileService.ts @@ -7,7 +7,7 @@ import type { TurnstileResponse } from '../models/TurnstileResponse'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; -export class TurnstilesService { +export class TurnstileService { /** * Get Turnstile Validation * Takes a query based on the 'TurnstileQuery' schema and retrieves data diff --git a/src/frontend/src/client/services/UnidentifiedItemsService.ts b/src/frontend/src/client/services/UnidentifiedItemService.ts similarity index 67% rename from src/frontend/src/client/services/UnidentifiedItemsService.ts rename to src/frontend/src/client/services/UnidentifiedItemService.ts index 48665ea4..c6112bdc 100644 --- a/src/frontend/src/client/services/UnidentifiedItemsService.ts +++ b/src/frontend/src/client/services/UnidentifiedItemService.ts @@ -7,7 +7,7 @@ import type { UnidentifiedItemCreate } from '../models/UnidentifiedItemCreate'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; -export class UnidentifiedItemsService { +export class UnidentifiedItemService { /** * Get Latest Item Id * Get the latest "itemId" @@ -83,4 +83,40 @@ export class UnidentifiedItemsService { }, }); } + /** + * Get Non Aggregated + * Get the non aggregated unidentified items for the last 10 recorded hours + * (not necessarily the last 10 hours) + * @returns UnidentifiedItem Successful Response + * @throws ApiError + */ + public static getNonAggregated(): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/api_v1/unidentifiedItem/non_aggregated/', + }); + } + /** + * Add Aggregated + * Deletes the non aggregated unidentified items for the last 10 recorded hours + * (not necessarily the last 10 hours) + * And inserts the new items + * @returns UnidentifiedItem Successful Response + * @throws ApiError + */ + public static addAggregated({ + requestBody, + }: { + requestBody: Array, + }): CancelablePromise> { + return __request(OpenAPI, { + method: 'POST', + url: '/api/api_v1/unidentifiedItem/add_aggregated/', + body: requestBody, + mediaType: 'application/json', + errors: { + 422: `Validation Error`, + }, + }); + } } diff --git a/src/frontend/src/components/Common/DateDaysHoursSinceLaunchStats.tsx b/src/frontend/src/components/Common/DateDaysHoursSinceLaunchStats.tsx index bd9737ea..57b1e7b2 100644 --- a/src/frontend/src/components/Common/DateDaysHoursSinceLaunchStats.tsx +++ b/src/frontend/src/components/Common/DateDaysHoursSinceLaunchStats.tsx @@ -4,42 +4,67 @@ import { useEffect, useState } from "react"; import { getHoursSinceLaunch, formatHoursSinceLaunch, - LEAGUE_LAUNCH_DATETIME, } from "../../hooks/graphing/utils"; -import { DEFAULT_LEAGUES } from "../../config"; import { setupHourlyUpdate } from "../../utils"; +import { useLeagueLaunchStats } from "../../store/LeagueLaunchStatsStore"; const DateDaysHoursSinceLaunchStats = (props: StatProps) => { - const defaultLeague = DEFAULT_LEAGUES[0]; + const [currentTime, setCurrentTime] = useState(new Date()); + const { league, leagueLaunch, hoursSinceLaunch, setHoursSinceLaunch } = + useLeagueLaunchStats(); + useEffect(() => { + return setupHourlyUpdate(setCurrentTime); + }, []); - const leagueLaunchDay = LEAGUE_LAUNCH_DATETIME.getDate(); - const leagueLaunchMonth = LEAGUE_LAUNCH_DATETIME.toLocaleString("default", { + const leagueLaunchDay = leagueLaunch.getDate(); + const leagueLaunchMonth = leagueLaunch.toLocaleString("default", { month: "short", }); + const leagueLaunchYear = leagueLaunch.toLocaleString("default", { + year: "numeric", + }); - const [currentTime, setCurrentTime] = useState(new Date()); const currentDay = currentTime.getDate(); const currentMonth = currentTime.toLocaleString("default", { month: "short", }); + const currentYear = currentTime.toLocaleString("default", { + year: "numeric", + }); - const hoursSinceLaunch = getHoursSinceLaunch(currentTime); - const daysHoursSinceLaunchFormat = formatHoursSinceLaunch(hoursSinceLaunch); - const [sinceLaunchDays, sinceLaunchHours] = - daysHoursSinceLaunchFormat.split("T"); - + const curHoursSinceLaunch = getHoursSinceLaunch(currentTime, leagueLaunch); + const [localHoursSinceLaunch, setLocalHoursSinceLaunch] = + useState(curHoursSinceLaunch); useEffect(() => { - return setupHourlyUpdate(setCurrentTime); - }, []); + const curHoursSinceLaunch = getHoursSinceLaunch(currentTime, leagueLaunch); + setLocalHoursSinceLaunch(hoursSinceLaunch); + if (curHoursSinceLaunch !== hoursSinceLaunch) { + setHoursSinceLaunch(curHoursSinceLaunch); + } + }, [ + leagueLaunch, + hoursSinceLaunch, + setHoursSinceLaunch, + currentTime, + localHoursSinceLaunch, + ]); + + const daysHoursSinceLaunchFormat = formatHoursSinceLaunch( + localHoursSinceLaunch, + ); + const [yearsDays, sinceLaunchHours] = daysHoursSinceLaunchFormat.split("T"); + const [sinceLaunchYears, sinceLaunchDays] = yearsDays.split("Y"); return ( - {sinceLaunchDays} days and {sinceLaunchHours} hours since{" "} - {defaultLeague} launched + {sinceLaunchYears !== "0" ? `${sinceLaunchYears} years, ` : ""} + {sinceLaunchDays} days and {sinceLaunchHours} hours since {league.name}{" "} + launched - {leagueLaunchMonth} {leagueLaunchDay} - {currentMonth} {currentDay} + {leagueLaunchMonth} {leagueLaunchDay} {leagueLaunchYear} -{" "} + {currentMonth} {currentDay} {currentYear} ); diff --git a/src/frontend/src/components/Common/MultiSelectButtonGrid.tsx b/src/frontend/src/components/Common/MultiSelectButtonGrid.tsx index 5bd7abf7..35501aea 100644 --- a/src/frontend/src/components/Common/MultiSelectButtonGrid.tsx +++ b/src/frontend/src/components/Common/MultiSelectButtonGrid.tsx @@ -1,6 +1,4 @@ -import { - Box, Flex, FlexProps -} from "@chakra-ui/layout"; +import { Box, Flex, FlexProps } from "@chakra-ui/layout"; import { Button } from "@chakra-ui/react"; import { capitalizeFirstLetter } from "../../hooks/utils"; import { TextWithUnderline } from "../Text/TextWithUnderline"; @@ -24,12 +22,18 @@ const MultiSelectButtonGrid = (props: MultiSelectProps) => { let defaultSelectedOptionsBoolean: boolean[]; if (defaultSelectedOptions !== undefined) { - defaultSelectedOptionsBoolean = options.map((val) => defaultSelectedOptions.includes(val)); + defaultSelectedOptionsBoolean = options.map((val) => + defaultSelectedOptions.includes(val), + ); } else { - defaultSelectedOptionsBoolean = new Array(numberOfOptions).fill(false); + defaultSelectedOptionsBoolean = new Array(numberOfOptions).fill( + false, + ); } - const [selectedOptions, setSelectedOptions] = useState(defaultSelectedOptionsBoolean); + const [selectedOptions, setSelectedOptions] = useState( + defaultSelectedOptionsBoolean, + ); const clearClicked = useGraphInputStore((state) => state.clearClicked); @@ -38,47 +42,48 @@ const MultiSelectButtonGrid = (props: MultiSelectProps) => { props.onClearClick(); setSelectedOptions(defaultSelectedOptionsBoolean); } - }, [clearClicked]) + }, [clearClicked, defaultSelectedOptionsBoolean, props]); const buttons = options.map((val, idx) => { - return - - - }) - return - - - {...buttons} + return ( + + + + ); + }); + return ( + + + + {...buttons} + - -} + ); +}; export default MultiSelectButtonGrid; diff --git a/src/frontend/src/components/Graph/CustomTooltip.tsx b/src/frontend/src/components/Graph/CustomTooltip.tsx index 75ca4458..f9514fcf 100644 --- a/src/frontend/src/components/Graph/CustomTooltip.tsx +++ b/src/frontend/src/components/Graph/CustomTooltip.tsx @@ -12,6 +12,7 @@ import { capitalizeFirstLetter } from "../../hooks/utils"; import { BiError } from "react-icons/bi"; import { getHoursSinceLaunch } from "../../hooks/graphing/utils"; import { setupHourlyUpdate } from "../../utils"; +import { useLeagueLaunchStats } from "../../store/LeagueLaunchStatsStore"; interface CustomTooltipProps extends TooltipContentProps { upperBoundry: number; @@ -21,10 +22,11 @@ interface CustomTooltipProps extends TooltipContentProps { export const CustomTooltip = (props: CustomTooltipProps) => { const [currentTime, setCurrentTime] = useState(new Date()); + const { leagueLaunch } = useLeagueLaunchStats(); let hoursAgo = -1; if (typeof props.label == "number") { - hoursAgo = getHoursSinceLaunch(currentTime) - props.label; + hoursAgo = getHoursSinceLaunch(currentTime, leagueLaunch) - props.label; } const daysToDisplay = Math.floor(hoursAgo / 24); diff --git a/src/frontend/src/components/Graph/GraphComponent.tsx b/src/frontend/src/components/Graph/GraphComponent.tsx index 8ab205ac..3e192fc6 100644 --- a/src/frontend/src/components/Graph/GraphComponent.tsx +++ b/src/frontend/src/components/Graph/GraphComponent.tsx @@ -33,7 +33,7 @@ import PlotCustomizationButtons from "../Common/PlotCustomizationButtons"; * and a graph if there has been */ function GraphComponent(props: BoxProps) { - const { plotQuery, leagues } = useGraphInputStore(); + const { plotQuery, choosableLeagues } = useGraphInputStore(); const { result: plotData, mostCommonCurrencyUsed, @@ -51,7 +51,10 @@ function GraphComponent(props: BoxProps) { const showSecondary = usePlotSettingsStore((state) => state.showSecondary); if (error || plotData == undefined) return; - const fetchedLeagues = plotData.data.map((val) => val.name); + const fetchedLeagueIds = plotData.data.map((val) => val.leagueId); + const fetchedLeagues = choosableLeagues + .filter((league) => fetchedLeagueIds.includes(league.leagueId)) + .map((league) => league.name); const isLowConfidence = confidenceRating === "low"; const isMediumConfidence = confidenceRating === "medium"; @@ -171,13 +174,13 @@ function GraphComponent(props: BoxProps) { {plotData.data.map( (series, idx) => - leagues.includes(series.name) && ( + fetchedLeagueIds.includes(series.leagueId) && ( - leagues.includes(series.name) && ( + fetchedLeagueIds.includes(series.leagueId) && ( { - const { leagues, addLeague, removeLeague } = useGraphInputStore(); - - - const selectLeagueOptions: string[] = [...DEFAULT_LEAGUES, ...ADDITIONAL_LEAGUES] + const { league } = useLeagueLaunchStats(); + const { leagues, choosableLeagues, addLeague, removeLeague } = + useGraphInputStore(); + const selectLeagueOptions: string[] = choosableLeagues.map( + (league) => league.name, + ); + let defaultLeagues; + if (leagues.length > 0) { + defaultLeagues = leagues; + } else { + defaultLeagues = league ? [league.name] : undefined; + } return ( useGraphInputStore.setState({ leagues: DEFAULT_LEAGUES })} + onClearClick={() => + useGraphInputStore.setState({ leagues: defaultLeagues }) + } /> - ) + ); }; diff --git a/src/frontend/src/config.tsx b/src/frontend/src/config.tsx index 22b134a8..2f9e9bbb 100644 --- a/src/frontend/src/config.tsx +++ b/src/frontend/src/config.tsx @@ -2,8 +2,5 @@ export const VITE_API_URL = import.meta.env.VITE_API_URL; export const TURNSTILE_SITE_KEY = import.meta.env.VITE_APP_TURNSTILE_SITE_KEY; -export const LEAGUE_LAUNCH_TIME = import.meta.env.VITE_APP_LEAGUE_LAUNCH_TIME; -export const PLOTTING_WINDOW_HOURS = import.meta.env.VITE_APP_PLOTTING_WINDOW_HOURS; - -export const DEFAULT_LEAGUES = (import.meta.env.VITE_APP_DEFAULT_LEAGUES).split("|") as Array; -export const ADDITIONAL_LEAGUES = (import.meta.env.VITE_APP_ADDITIONAL_LEAGUES).split("|") as Array; +export const PLOTTING_WINDOW_HOURS = import.meta.env + .VITE_APP_PLOTTING_WINDOW_HOURS; diff --git a/src/frontend/src/hooks/getData/getBaseTypeCategories.tsx b/src/frontend/src/hooks/getData/getBaseTypeCategories.tsx index 202b845e..a4fdfc7a 100644 --- a/src/frontend/src/hooks/getData/getBaseTypeCategories.tsx +++ b/src/frontend/src/hooks/getData/getBaseTypeCategories.tsx @@ -1,54 +1,50 @@ -import { useQueryClient } from "@tanstack/react-query"; -import { ItemBaseTypesService, ItemBaseType } from "../../client"; +import { QueryClient } from "@tanstack/react-query"; +import { ItemBaseTypeService, ItemBaseType } from "../../client"; // Fetches all item base type data and processes related unique item names -export const useGetItemBaseTypes = async () => { - const queryClient = useQueryClient(); - - try { - // Fetch and return item base types directly - const itemBaseTypes = await queryClient.fetchQuery({ - queryKey: ["baseTypeValues"], - queryFn: async () => { - const data = await ItemBaseTypesService.getAllItemBaseTypes({}); - return Array.isArray(data) ? data : [data]; - }, - }); - - const createItemNameArray = (itemBaseType: ItemBaseType[]): string[] => { - const reduceItemNameArray = ( - prev: string[] | undefined, - cur: string[] | undefined - ): string[] => { - prev = prev || []; - if (cur) { - const newItemNames = cur.filter((value) => !prev.includes(value)); - prev.push(...newItemNames); - } - return prev; - }; - - const arrayOfRelatedUniques = itemBaseType.map((baseType) => { - const relatedUniques = baseType.relatedUniques; - return relatedUniques ? relatedUniques.split("|") : []; - }); - - return arrayOfRelatedUniques.reduce(reduceItemNameArray, []); - }; - - const itemNames = createItemNameArray(itemBaseTypes); - - return { - itemBaseTypes, - itemNames, - }; - - } catch (error) { - console.error("Error fetching item base types:", error); - return { - itemBaseTypes: [], - itemNames: [], - }; - } +export const useGetItemBaseTypes = async (queryClient: QueryClient) => { + try { + // Fetch and return item base types directly + const itemBaseTypes = await queryClient.fetchQuery({ + queryKey: ["baseTypeValues"], + queryFn: async () => { + const data = await ItemBaseTypeService.getAllItemBaseTypes({}); + return Array.isArray(data) ? data : [data]; + }, + }); + + const createItemNameArray = (itemBaseType: ItemBaseType[]): string[] => { + const reduceItemNameArray = ( + prev: string[] | undefined, + cur: string[] | undefined, + ): string[] => { + prev = prev || []; + if (cur) { + const newItemNames = cur.filter((value) => !prev.includes(value)); + prev.push(...newItemNames); + } + return prev; + }; + + const arrayOfRelatedUniques = itemBaseType.map((baseType) => { + const relatedUniques = baseType.relatedUniques; + return relatedUniques ? relatedUniques.split("|") : []; + }); + + return arrayOfRelatedUniques.reduce(reduceItemNameArray, []); + }; + + const itemNames = createItemNameArray(itemBaseTypes); + + return { + itemBaseTypes, + itemNames, + }; + } catch (error) { + console.error("Error fetching item base types:", error); + return { + itemBaseTypes: [], + itemNames: [], + }; + } }; - diff --git a/src/frontend/src/hooks/getData/getGroupedModifiers.tsx b/src/frontend/src/hooks/getData/getGroupedModifiers.tsx index cc919d70..fc898b92 100644 --- a/src/frontend/src/hooks/getData/getGroupedModifiers.tsx +++ b/src/frontend/src/hooks/getData/getGroupedModifiers.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { ModifiersService, GroupedModifierByEffect } from "../../client"; +import { ModifierService, GroupedModifierByEffect } from "../../client"; import { useQuery } from "@tanstack/react-query"; // Get all grouped modifiers by effect @@ -11,9 +11,7 @@ export const GetGroupedModifiersByEffect = () => { useQuery({ queryKey: ["allModifiers"], queryFn: async () => { - setModifiers( - await ModifiersService.getGroupedModifierByEffect() - ); + setModifiers(await ModifierService.getGroupedModifierByEffect()); return 1; }, }); diff --git a/src/frontend/src/hooks/getData/getLeagues.tsx b/src/frontend/src/hooks/getData/getLeagues.tsx new file mode 100644 index 00000000..ca4efd8b --- /dev/null +++ b/src/frontend/src/hooks/getData/getLeagues.tsx @@ -0,0 +1,18 @@ +import { LeagueService } from "../../client"; +import { QueryClient } from "@tanstack/react-query"; + +export const useGetLeagues = async (queryClient: QueryClient) => { + try { + const leagues = await queryClient.fetchQuery({ + queryKey: ["allModifiers"], + queryFn: async () => { + const data = await LeagueService.getAllLeagues({}); + return Array.isArray(data) ? data : [data]; + }, + }); + return { leagues }; + } catch (error) { + console.log(error); + return { leagues: [] }; + } +}; diff --git a/src/frontend/src/hooks/getData/prefetchGroupedModifiers.tsx b/src/frontend/src/hooks/getData/prefetchGroupedModifiers.tsx index a8396f8f..19c3efac 100644 --- a/src/frontend/src/hooks/getData/prefetchGroupedModifiers.tsx +++ b/src/frontend/src/hooks/getData/prefetchGroupedModifiers.tsx @@ -1,23 +1,22 @@ import { useQueryClient } from "@tanstack/react-query"; -import { ModifiersService } from "../../client"; +import { ModifierService } from "../../client"; export const useGetGroupedModifiers = async () => { - const queryClient = useQueryClient(); + const queryClient = useQueryClient(); - try { - // Fetch and return the grouped modifiers directly - const groupedModifiers = await queryClient.fetchQuery({ - queryKey: ["prefetchedGroupedModifiers"], - queryFn: async () => { - const fetchedData = - await ModifiersService.getGroupedModifierByEffect(); - return Array.isArray(fetchedData) ? fetchedData : [fetchedData]; - }, - }); + try { + // Fetch and return the grouped modifiers directly + const groupedModifiers = await queryClient.fetchQuery({ + queryKey: ["prefetchedGroupedModifiers"], + queryFn: async () => { + const fetchedData = await ModifierService.getGroupedModifierByEffect(); + return Array.isArray(fetchedData) ? fetchedData : [fetchedData]; + }, + }); - return { groupedModifiers }; - } catch (error) { - console.log(error); - return { groupedModifiers: [] }; // Return an empty array on error - } + return { groupedModifiers }; + } catch (error) { + console.log(error); + return { groupedModifiers: [] }; // Return an empty array on error + } }; diff --git a/src/frontend/src/hooks/graphing/postPlottingData.tsx b/src/frontend/src/hooks/graphing/postPlottingData.tsx index 9ba8ea10..54c8de8e 100755 --- a/src/frontend/src/hooks/graphing/postPlottingData.tsx +++ b/src/frontend/src/hooks/graphing/postPlottingData.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { PlotsService, PlotQuery, PlotData } from "../../client"; +import { PlotService, PlotQuery, PlotData } from "../../client"; import { useQuery } from "@tanstack/react-query"; import { useGraphInputStore } from "../../store/GraphInputStore"; @@ -22,7 +22,7 @@ function usePostPlottingData(plotQuery: PlotQuery): { const { fetchStatus, refetch, isFetched, isError, error } = useQuery({ queryKey: ["allPlotData"], queryFn: async () => { - const returnBody = await PlotsService.getPlotData({ + const returnBody = await PlotService.getPlotData({ requestBody: plotQuery, }); diff --git a/src/frontend/src/hooks/graphing/processPlottingData.tsx b/src/frontend/src/hooks/graphing/processPlottingData.tsx index 8549e583..fe87959e 100644 --- a/src/frontend/src/hooks/graphing/processPlottingData.tsx +++ b/src/frontend/src/hooks/graphing/processPlottingData.tsx @@ -3,8 +3,9 @@ import { PlotQuery } from "../../client"; import { FilledPlotData } from "../../schemas/Datum"; import { useEffect } from "react"; import useCustomToast from "../useCustomToast"; -import { findWinsorUpperBound, getHoursSinceLaunch } from "./utils"; +import { findWinsorUpperBound } from "./utils"; import { PLOTTING_WINDOW_HOURS } from "../../config"; +import { useLeagueLaunchStats } from "../../store/LeagueLaunchStatsStore"; /** * A hook that takes the current plot query and returns @@ -22,6 +23,7 @@ function useGetPlotData(plotQuery: PlotQuery): { } { const { plotData, fetchStatus, isFetched, isError, error } = usePostPlottingData(plotQuery); + const { hoursSinceLaunch } = useLeagueLaunchStats(); const showToast = useCustomToast(); useEffect(() => { if (isError && isFetched) { @@ -33,39 +35,38 @@ function useGetPlotData(plotQuery: PlotQuery): { if (plotData !== undefined) { const mostCommonCurrencyUsed = plotData.mostCommonCurrencyUsed; - const currentHour = getHoursSinceLaunch(new Date()); - const firstHour = currentHour - PLOTTING_WINDOW_HOURS; + const firstHour = hoursSinceLaunch - PLOTTING_WINDOW_HOURS; const filledPlotData: FilledPlotData = { mostCommonCurrencyUsed: mostCommonCurrencyUsed, - data: plotData.data.map( - (val) => ( - { - confidenceRating: val.confidenceRating, - name: val.name, - data: [] - })) + data: plotData.data.map((val) => ({ + confidenceRating: val.confidenceRating, + leagueId: val.leagueId, + data: [], + })), }; - const currentIdxArray = plotData.data.reduce((prev) => [...prev, 0], [] as number[]); + const currentIdxArray = plotData.data.reduce( + (prev) => [...prev, 0], + [] as number[], + ); let currentIdx: number; // Makes a filled plot data object, meaning that every hour has an entry // Missing entries from the original plot data is replaced by a "null" entry - for (let hour = firstHour; hour <= currentHour; hour++) { + for (let hour = firstHour; hour <= hoursSinceLaunch; hour++) { for (let seriesIdx = 0; seriesIdx < currentIdxArray.length; seriesIdx++) { currentIdx = currentIdxArray[seriesIdx]; if ( plotData.data[seriesIdx].data.length <= currentIdx || plotData.data[seriesIdx].data[currentIdx].hoursSinceLaunch !== hour ) { - filledPlotData.data[seriesIdx].data.push( - { - hoursSinceLaunch: hour, - valueInChaos: null, - valueInMostCommonCurrencyUsed: null, - confidence: null - }) + filledPlotData.data[seriesIdx].data.push({ + hoursSinceLaunch: hour, + valueInChaos: null, + valueInMostCommonCurrencyUsed: null, + confidence: null, + }); } else { filledPlotData.data[seriesIdx].data.push( - plotData.data[seriesIdx].data[currentIdx] + plotData.data[seriesIdx].data[currentIdx], ); currentIdxArray[seriesIdx] = currentIdx + 1; } @@ -74,14 +75,14 @@ function useGetPlotData(plotQuery: PlotQuery): { const upperBoundryChaos = findWinsorUpperBound( plotData.data[0].data.reduce( (prev, cur) => [...prev, cur.valueInChaos], - [] as number[] - ) + [] as number[], + ), ); const upperBoundrySecondary = findWinsorUpperBound( plotData.data[0].data.reduce( (prev, cur) => [...prev, cur.valueInMostCommonCurrencyUsed], - [] as number[] - ) + [] as number[], + ), ); const confidenceRating = plotData.data[0].confidenceRating; return { @@ -91,8 +92,8 @@ function useGetPlotData(plotQuery: PlotQuery): { upperBoundryChaos, upperBoundrySecondary, fetchStatus, - error - } + error, + }; } return { result: undefined, diff --git a/src/frontend/src/hooks/graphing/utils.tsx b/src/frontend/src/hooks/graphing/utils.tsx index aa08cfd0..1d5bc2ce 100644 --- a/src/frontend/src/hooks/graphing/utils.tsx +++ b/src/frontend/src/hooks/graphing/utils.tsx @@ -1,4 +1,4 @@ -import { PlotQuery } from "../../client"; +import { League, PlotQuery } from "../../client"; import { useErrorStore } from "../../store/ErrorStore"; import { useGraphInputStore } from "../../store/GraphInputStore"; import { @@ -6,9 +6,31 @@ import { GraphInputState, ModifierLimitationState, } from "../../store/StateInterface"; -import { LEAGUE_LAUNCH_TIME, PLOTTING_WINDOW_HOURS } from "../../config"; +import { PLOTTING_WINDOW_HOURS } from "../../config"; +import { useLeagueLaunchStats } from "../../store/LeagueLaunchStatsStore"; -export const LEAGUE_LAUNCH_DATETIME = new Date(LEAGUE_LAUNCH_TIME); +export const getCurrentLeague = (choosableLeagues: League | League[]) => { + choosableLeagues = Array.isArray(choosableLeagues) + ? choosableLeagues + : [choosableLeagues]; + const now = Date.now(); + const currentLeague = choosableLeagues + .filter((league) => !league.name.startsWith("Hardcore")) + .reduce((latest, league) => { + const validFrom = Date.parse(league.validFrom); + const validTo = league.validTo + ? Date.parse(league.validTo) + : Number.POSITIVE_INFINITY; + if (validFrom > now || now > validTo) { + return latest; + } + if (!latest || validFrom > Date.parse(latest.validFrom)) { + return league; + } + return latest; + }, undefined); + return currentLeague; +}; const calcMean = (values: number[]) => { const sumValues = values.reduce((prev, cur) => prev + cur, 0); @@ -31,17 +53,22 @@ export const findWinsorUpperBound = (values: number[]) => { }; export function formatHoursSinceLaunch(hoursSinceLaunch: number): string { - const daysSinceLaunch = Math.floor(hoursSinceLaunch / 24); + let daysSinceLaunch = Math.floor(hoursSinceLaunch / 24); + const yearsSinceLaunch = Math.floor(daysSinceLaunch / 365); + daysSinceLaunch = daysSinceLaunch - yearsSinceLaunch * 365; const remainder = hoursSinceLaunch % 24; // Combine the date and time into the desired format - return `${daysSinceLaunch}T${remainder}`; + return `${yearsSinceLaunch}Y${daysSinceLaunch}T${remainder}`; } -export const getHoursSinceLaunch = (currentTime: Date): number => { +export const getHoursSinceLaunch = ( + currentTime: Date, + leagueLaunch: Date, +): number => { const getCurrentTimeDate = currentTime.getTime(); const hoursSinceLaunch = Math.floor( - (getCurrentTimeDate - LEAGUE_LAUNCH_DATETIME.getTime()) / (1000 * 3600), + (getCurrentTimeDate - leagueLaunch.getTime()) / (1000 * 3600), ); return hoursSinceLaunch; }; @@ -238,13 +265,14 @@ export const getOptimizedPlotQuery = (): PlotQuery | undefined => { modifierId: wantedMoidifierExtended.modifierId, modifierLimitations: wantedMoidifierExtended.modifierLimitations, })); - const currentTime = new Date(); - const end = getHoursSinceLaunch(currentTime); + const end = useLeagueLaunchStats.getState().hoursSinceLaunch; const window = PLOTTING_WINDOW_HOURS; const start = end - window; return { - league: state.leagues, + leagueId: state.choosableLeagues + .filter((league) => state.leagues.includes(league.name)) + .map((league) => league.leagueId), itemSpecifications: itemSpec, baseSpecifications: baseSpec, wantedModifiers: wantedModifier, diff --git a/src/frontend/src/hooks/validation/turnstileValidation.tsx b/src/frontend/src/hooks/validation/turnstileValidation.tsx index b95f8bd8..7c98996d 100644 --- a/src/frontend/src/hooks/validation/turnstileValidation.tsx +++ b/src/frontend/src/hooks/validation/turnstileValidation.tsx @@ -3,7 +3,7 @@ import { ApiError, TurnstileQuery, TurnstileResponse, - TurnstilesService, + TurnstileService, } from "../../client"; import { useNavigate } from "@tanstack/react-router"; import { useState } from "react"; @@ -37,7 +37,7 @@ const useTurnstileValidation = (requestBody?: TurnstileQuery) => { } = useQuery({ queryKey: ["turnstile", hasCompletedCaptcha()], queryFn: async (): Promise => { - const turnstileResponse = await TurnstilesService.getTurnstileValidation({ + const turnstileResponse = await TurnstileService.getTurnstileValidation({ requestBody: { token: requestBody?.token ?? token, ip: requestBody?.ip ?? ip, @@ -62,7 +62,7 @@ const useTurnstileValidation = (requestBody?: TurnstileQuery) => { const completeCaptcha = async (data: TurnstileQuery) => { console.log("Completing captcha..."); - await TurnstilesService.getTurnstileValidation({ + await TurnstileService.getTurnstileValidation({ requestBody: data, }); }; @@ -74,7 +74,7 @@ const useTurnstileValidation = (requestBody?: TurnstileQuery) => { setToken(requestBody?.token ?? ""); localStorage.setItem( "turnstile_captcha_token", - requestBody?.token ?? token + requestBody?.token ?? token, ); navigate({ to: from ? "/" + `${from}` : "/" }); }, diff --git a/src/frontend/src/routes/_layout/index.tsx b/src/frontend/src/routes/_layout/index.tsx index 64d366d0..db2ad685 100644 --- a/src/frontend/src/routes/_layout/index.tsx +++ b/src/frontend/src/routes/_layout/index.tsx @@ -1,47 +1,76 @@ -import { useEffect, useRef } from 'react' -import { createFileRoute } from '@tanstack/react-router' +import { useEffect, useRef } from "react"; +import { createFileRoute } from "@tanstack/react-router"; -import { MainPage } from '../../components/Common/MainPage' -import { useGraphInputStore } from '../../store/GraphInputStore' -import { useGetGroupedModifiers } from '../../hooks/getData/prefetchGroupedModifiers' -import { useGetItemBaseTypes } from '../../hooks/getData/getBaseTypeCategories' -import { validateAndSetSearchParams } from '../../utils' +import { MainPage } from "../../components/Common/MainPage"; +import { useGraphInputStore } from "../../store/GraphInputStore"; +import { useGetGroupedModifiers } from "../../hooks/getData/prefetchGroupedModifiers"; +import { useGetItemBaseTypes } from "../../hooks/getData/getBaseTypeCategories"; +import { useGetLeagues } from "../../hooks/getData/getLeagues"; +import { validateAndSetSearchParams } from "../../utils"; +import { getCurrentLeague } from "../../hooks/graphing/utils"; +import { useLeagueLaunchStats } from "../../store/LeagueLaunchStatsStore"; +import { useQueryClient } from "@tanstack/react-query"; export const Route = createFileRoute("/_layout/")({ beforeLoad: async () => { - const searchParams = new URLSearchParams(location.hash.slice(1)) - validateAndSetSearchParams(searchParams) + // const searchParams = new URLSearchParams(location.hash.slice(1)); + validateAndSetSearchParams(); }, component: Index, -}) +}); // Index Component - This component is the main component for the index route. function Index() { const { setChoosableModifiers, setChoosableItemBaseType, + addLeague, + setChoosableLeagues, setChoosableItemNames, getStoreFromHash, - } = useGraphInputStore() - const requestGroupedModifiers = useGetGroupedModifiers() - const requestItemBaseTypes = useGetItemBaseTypes() - const isFetched = useRef(false) + setHashFromStore, + } = useGraphInputStore(); + const { setLeague, setLeagueLaunch } = useLeagueLaunchStats(); + const queryClient = useQueryClient(); + const requestGroupedModifiers = useGetGroupedModifiers(); + const requestItemBaseTypes = useGetItemBaseTypes(queryClient); + const requestLeagues = useGetLeagues(queryClient); + const isFetched = useRef(false); useEffect(() => { if (!isFetched.current) { const fetchData = async () => { - const groupedModifiers = await requestGroupedModifiers - const itemBaseTypes = await requestItemBaseTypes + const groupedModifiers = await requestGroupedModifiers; + const itemBaseTypes = await requestItemBaseTypes; + const leagues = await requestLeagues; if (groupedModifiers) { - setChoosableModifiers(groupedModifiers.groupedModifiers) + setChoosableModifiers(groupedModifiers.groupedModifiers); } if (itemBaseTypes) { - setChoosableItemBaseType(itemBaseTypes.itemBaseTypes) - setChoosableItemNames(itemBaseTypes.itemNames) + setChoosableItemBaseType(itemBaseTypes.itemBaseTypes); + setChoosableItemNames(itemBaseTypes.itemNames); } - isFetched.current = true - } - fetchData() + if (leagues) { + setChoosableLeagues( + leagues.leagues.sort( + (league1, league2) => + Date.parse(league2.validFrom) - Date.parse(league1.validFrom), + ), + ); + const currentLeague = getCurrentLeague(leagues.leagues); + if (currentLeague !== undefined) { + const leagueLaunch = new Date(currentLeague.validFrom); + setLeagueLaunch(leagueLaunch); + // setLeague is used to create the header + setLeague(currentLeague); + // addLeague is for the plot query, and needed for default league selection + addLeague(currentLeague.name); + setHashFromStore(); + } + } + isFetched.current = true; + }; + fetchData(); } }, [ requestGroupedModifiers, @@ -50,6 +79,12 @@ function Index() { setChoosableItemBaseType, setChoosableItemNames, getStoreFromHash, - ]) - return + requestLeagues, + setChoosableLeagues, + setLeagueLaunch, + setLeague, + addLeague, + setHashFromStore, + ]); + return ; } diff --git a/src/frontend/src/schemas/Datum.tsx b/src/frontend/src/schemas/Datum.tsx index d79c11fa..766282ae 100644 --- a/src/frontend/src/schemas/Datum.tsx +++ b/src/frontend/src/schemas/Datum.tsx @@ -6,12 +6,12 @@ type Datum = { valueInChaos: number | null; valueInMostCommonCurrencyUsed: number | null; confidence: "low" | "medium" | "high" | null; -} +}; type TimeseriesData = { - name: string; + leagueId: number; data: Array; - confidenceRating: 'low' | 'medium' | 'high'; + confidenceRating: "low" | "medium" | "high"; }; export type FilledPlotData = { diff --git a/src/frontend/src/store/GraphInputStore.tsx b/src/frontend/src/store/GraphInputStore.tsx index fad2acf9..5f5e2397 100644 --- a/src/frontend/src/store/GraphInputStore.tsx +++ b/src/frontend/src/store/GraphInputStore.tsx @@ -9,9 +9,13 @@ import { WantedModifier, WantedModifierExtended, } from "./StateInterface"; -import { GroupedModifierByEffect, ItemBaseType, PlotQuery } from "../client"; +import { + GroupedModifierByEffect, + ItemBaseType, + League, + PlotQuery, +} from "../client"; import { encodeHash, decodeHash } from "./utils"; -import { DEFAULT_LEAGUES } from "../config"; import { updateNumericalRoll } from "../hooks/graphing/utils"; // Graph Input Store - This store is used to store graph input data. @@ -26,7 +30,8 @@ export const useGraphInputStore = create((set) => ({ choosableItemBaseType: [], choosableItemNames: [], - leagues: [DEFAULT_LEAGUES[0]], + leagues: [], + choosableLeagues: [], itemName: undefined, itemSpec: undefined, @@ -35,7 +40,7 @@ export const useGraphInputStore = create((set) => ({ wantedModifierExtended: [], plotQuery: { - league: DEFAULT_LEAGUES, + leagueId: -1, wantedModifiers: [], }, @@ -128,6 +133,10 @@ export const useGraphInputStore = create((set) => ({ set(() => ({ choosableItemNames: choosableItemNames, })), + setChoosableLeagues: (leagues: League[]) => + set(() => ({ + choosableLeagues: leagues, + })), updateChoosable: (itemName: string | undefined) => set((state) => { @@ -192,7 +201,6 @@ export const useGraphInputStore = create((set) => ({ set(() => ({ leagues: [], })), - setItemSpec: (itemSpec: ItemSpecState) => set(() => ({ itemSpec: itemSpec })), setItemSpecIdentified: (identified: boolean | undefined) => diff --git a/src/frontend/src/store/LeagueLaunchStatsStore.tsx b/src/frontend/src/store/LeagueLaunchStatsStore.tsx new file mode 100644 index 00000000..223065bf --- /dev/null +++ b/src/frontend/src/store/LeagueLaunchStatsStore.tsx @@ -0,0 +1,19 @@ +import { create } from "zustand"; +import { LeagueLaunchStats } from "./StateInterface"; +import { League } from "../client"; + +export const useLeagueLaunchStats = create((set) => ({ + league: { + name: "Path of Exile", + validFrom: "2013-10-23T21:00:00Z", + leagueId: -1, + version: 1.0, + }, + leagueLaunch: new Date("2013-10-23T21:00:00Z"), + hoursSinceLaunch: Number.NEGATIVE_INFINITY, + setLeague: (league: League) => set(() => ({ league: league })), + setLeagueLaunch: (leagueLaunch: Date) => + set(() => ({ leagueLaunch: leagueLaunch })), + setHoursSinceLaunch: (hoursSinceLaunch: number) => + set(() => ({ hoursSinceLaunch: hoursSinceLaunch })), +})); diff --git a/src/frontend/src/store/StateInterface.tsx b/src/frontend/src/store/StateInterface.tsx index 2d6c2543..deeb6816 100644 --- a/src/frontend/src/store/StateInterface.tsx +++ b/src/frontend/src/store/StateInterface.tsx @@ -1,10 +1,21 @@ import { GroupedModifierByEffect, ItemBaseType, + League, PlotQuery, TurnstileResponse, } from "../client"; +export interface LeagueLaunchStats { + league: League; + leagueLaunch: Date; + hoursSinceLaunch: number; + + setLeague: (league: League) => void; + setLeagueLaunch: (leagueLaunch: Date) => void; + setHoursSinceLaunch: (hoursSinceLaunch: number) => void; +} + export interface ChoosableModifiersExtended extends GroupedModifierByEffect { isNotChoosable?: boolean; } @@ -90,6 +101,7 @@ export interface GraphInputState { choosableItemNames: string[]; leagues: string[]; + choosableLeagues: League[]; itemName: string | undefined; itemSpec: ItemSpecState | undefined; @@ -119,6 +131,7 @@ export interface GraphInputState { addLeague: (league: string) => void; removeLeague: (league: string) => void; removeAllLeagues: () => void; + setChoosableLeagues: (leagues: League[]) => void; setItemName: (name: string | undefined) => void; diff --git a/src/frontend/src/utils.ts b/src/frontend/src/utils.ts index 3e28cbdc..ffec8339 100644 --- a/src/frontend/src/utils.ts +++ b/src/frontend/src/utils.ts @@ -1,6 +1,5 @@ import type { ApiError } from "./client"; import { Toast } from "./hooks/useCustomToast"; -import { DEFAULT_LEAGUES } from "./config"; import { useGraphInputStore } from "./store/GraphInputStore"; export const emailPattern = { @@ -85,30 +84,8 @@ export const setupHourlyUpdate = (setCurrentTime: SetDateFunction) => { return () => clearTimeout(timeoutId); }; -const validateLeagues = (searchParams: URLSearchParams) => { - const leagues = searchParams.get("league"); - if (searchParams.size === 1 && leagues) { - if (leagues.length > 1 || !leagues.includes(DEFAULT_LEAGUES[0])) { - throw "default league not in simple url"; - } - } else if (!leagues || leagues.length === 0) { - throw "leagues not set in url"; - } -}; - -export const validateAndSetSearchParams = (searchParams: URLSearchParams) => { +export const validateAndSetSearchParams = () => { const graphState = useGraphInputStore.getState(); - try { - graphState.setHashFromStore(); - validateLeagues(searchParams); - } catch (error) { - const graphState = useGraphInputStore.getState(); - graphState.removeAllLeagues(); - graphState.addLeague(DEFAULT_LEAGUES[0]); - const searchParams = new URLSearchParams(); - searchParams.set("league", DEFAULT_LEAGUES[0]); - graphState.setHashFromStore(); - } finally { - graphState.setStateHash(undefined); - } + graphState.setHashFromStore(); + graphState.setStateHash(undefined); };