Skip to content

Commit 35eb864

Browse files
authored
Add ltd2 client (#1)
Add ltd2 client and model, loading data from ltd2 api ready
1 parent e780541 commit 35eb864

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1031
-309
lines changed

.env.template

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,6 @@ SENTRY_DSN=
3636
# Configure these with your own Docker registry images
3737
DOCKER_IMAGE_BACKEND=backend
3838
DOCKER_IMAGE_FRONTEND=frontend
39+
40+
LTD2_X_API_KEY=changethis
41+
LTD2_ICONS_PATH=changethis

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,41 @@
1+
# LTD2-DSS
2+
copy env file
3+
```bash
4+
cp .env.template .env
5+
```
6+
7+
Fill env variable with your API key from https://developer.legiontd2.com/home - key_management
8+
```
9+
LTD2_X_API_KEY=xxx
10+
```
11+
Fill env variable with your path to game icons, use following example:
12+
```
13+
LTD2_ICONS_PATH="/home/maciej/.steam/debian-installation/steamapps/common/Legion TD 2/Legion TD 2_Data/uiresources/AeonGT/hud/img/icons/"
14+
```
15+
16+
feed environment variables from .env
17+
```bash
18+
source .env
19+
```
20+
change owner of files
21+
```bash
22+
sudo chown -R $(whoami) .
23+
```
24+
25+
open psql where a84 is postgres container id
26+
```bash
27+
docker exec -it a84 psql -U postgres -d app
28+
```
29+
clear docker
30+
```bash
31+
docker system prune -a --volumes
32+
```
33+
34+
run project
35+
```bash
36+
docker compose up -d
37+
```
38+
139
# Full Stack FastAPI Template
240

341
<a href="https://github.com/tiangolo/full-stack-fastapi-template/actions?query=workflow%3ATest" target="_blank"><img src="https://github.com/tiangolo/full-stack-fastapi-template/workflows/Test/badge.svg" alt="Test"></a>

backend/app/api/deps.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from fastapi import Depends, HTTPException, status
55
from fastapi.security import OAuth2PasswordBearer
66
from jose import JWTError, jwt
7+
from playwright.async_api import APIRequestContext
78
from pydantic import ValidationError
89
from sqlmodel import Session
910

11+
from app.clients.ltd2 import get_api_request_context
1012
from app.core import security
1113
from app.core.config import settings
1214
from app.core.db import engine
@@ -23,6 +25,7 @@ def get_db() -> Generator[Session, None, None]:
2325

2426

2527
SessionDep = Annotated[Session, Depends(get_db)]
28+
APIRequestContextDep = Annotated[APIRequestContext, Depends(get_api_request_context)]
2629
TokenDep = Annotated[str, Depends(reusable_oauth2)]
2730

2831

@@ -51,6 +54,6 @@ def get_current_user(session: SessionDep, token: TokenDep) -> User:
5154
def get_current_active_superuser(current_user: CurrentUser) -> User:
5255
if not current_user.is_superuser:
5356
raise HTTPException(
54-
status_code=400, detail="The user doesn't have enough privileges"
57+
status_code=403, detail="The user doesn't have enough privileges"
5558
)
5659
return current_user

backend/app/api/main.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from fastapi import APIRouter
22

3-
from app.api.routes import items, login, users, utils
3+
from app.api.routes import items, login, ltd2_units, users, utils
44

55
api_router = APIRouter()
66
api_router.include_router(login.router, tags=["login"])
77
api_router.include_router(users.router, prefix="/users", tags=["users"])
88
api_router.include_router(utils.router, prefix="/utils", tags=["utils"])
99
api_router.include_router(items.router, prefix="/items", tags=["items"])
10+
api_router.include_router(ltd2_units.router, prefix="/ltd2-units", tags=["ltd2-units"])

backend/app/api/routes/items.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
from sqlmodel import func, select
55

66
from app.api.deps import CurrentUser, SessionDep
7-
from app.models import Item, ItemCreate, ItemOut, ItemsOut, ItemUpdate, Message
7+
from app.models import Item, ItemCreate, ItemPublic, ItemsPublic, ItemUpdate, Message
88

99
router = APIRouter()
1010

1111

12-
@router.get("/", response_model=ItemsOut)
12+
@router.get("/", response_model=ItemsPublic)
1313
def read_items(
1414
session: SessionDep, current_user: CurrentUser, skip: int = 0, limit: int = 100
1515
) -> Any:
@@ -37,10 +37,10 @@ def read_items(
3737
)
3838
items = session.exec(statement).all()
3939

40-
return ItemsOut(data=items, count=count)
40+
return ItemsPublic(data=items, count=count)
4141

4242

43-
@router.get("/{id}", response_model=ItemOut)
43+
@router.get("/{id}", response_model=ItemPublic)
4444
def read_item(session: SessionDep, current_user: CurrentUser, id: int) -> Any:
4545
"""
4646
Get item by ID.
@@ -53,7 +53,7 @@ def read_item(session: SessionDep, current_user: CurrentUser, id: int) -> Any:
5353
return item
5454

5555

56-
@router.post("/", response_model=ItemOut)
56+
@router.post("/", response_model=ItemPublic)
5757
def create_item(
5858
*, session: SessionDep, current_user: CurrentUser, item_in: ItemCreate
5959
) -> Any:
@@ -67,7 +67,7 @@ def create_item(
6767
return item
6868

6969

70-
@router.put("/{id}", response_model=ItemOut)
70+
@router.put("/{id}", response_model=ItemPublic)
7171
def update_item(
7272
*, session: SessionDep, current_user: CurrentUser, id: int, item_in: ItemUpdate
7373
) -> Any:

backend/app/api/routes/login.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from app.core import security
1111
from app.core.config import settings
1212
from app.core.security import get_password_hash
13-
from app.models import Message, NewPassword, Token, UserOut
13+
from app.models import Message, NewPassword, Token, UserPublic
1414
from app.utils import (
1515
generate_password_reset_token,
1616
generate_reset_password_email,
@@ -43,7 +43,7 @@ def login_access_token(
4343
)
4444

4545

46-
@router.post("/login/test-token", response_model=UserOut)
46+
@router.post("/login/test-token", response_model=UserPublic)
4747
def test_token(current_user: CurrentUser) -> Any:
4848
"""
4949
Test access token
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from collections.abc import Sequence
2+
from http import HTTPStatus
3+
4+
from fastapi import APIRouter, Query
5+
from sqlmodel import select
6+
7+
from app.api.deps import APIRequestContextDep, SessionDep
8+
from app.clients.ltd2 import (
9+
find_units_by_version,
10+
get_current_game_version,
11+
)
12+
from app.models import LTD2Unit
13+
14+
router = APIRouter()
15+
16+
17+
@router.post("/", status_code=HTTPStatus.CREATED)
18+
async def clone_units_from_ltd2_api(
19+
session: SessionDep,
20+
api_request_context: APIRequestContextDep,
21+
) -> None:
22+
version = await get_current_game_version(api_request_context=api_request_context)
23+
units_list = await find_units_by_version(
24+
version=version, api_request_context=api_request_context
25+
)
26+
ltd2_units = [LTD2Unit.model_validate(unit) for unit in units_list]
27+
28+
session.bulk_save_objects(ltd2_units)
29+
session.commit()
30+
session.expire_all()
31+
32+
33+
@router.get("/")
34+
async def read_units(
35+
session: SessionDep, offset: int = 0, limit: int = Query(default=10, le=500)
36+
) -> Sequence[LTD2Unit]:
37+
units = session.exec(select(LTD2Unit).offset(offset).limit(limit)).all()
38+
return units

backend/app/api/routes/users.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@
1717
UpdatePassword,
1818
User,
1919
UserCreate,
20-
UserOut,
20+
UserPublic,
2121
UserRegister,
22-
UsersOut,
22+
UsersPublic,
2323
UserUpdate,
2424
UserUpdateMe,
2525
)
@@ -29,7 +29,9 @@
2929

3030

3131
@router.get(
32-
"/", dependencies=[Depends(get_current_active_superuser)], response_model=UsersOut
32+
"/",
33+
dependencies=[Depends(get_current_active_superuser)],
34+
response_model=UsersPublic,
3335
)
3436
def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
3537
"""
@@ -42,11 +44,11 @@ def read_users(session: SessionDep, skip: int = 0, limit: int = 100) -> Any:
4244
statement = select(User).offset(skip).limit(limit)
4345
users = session.exec(statement).all()
4446

45-
return UsersOut(data=users, count=count)
47+
return UsersPublic(data=users, count=count)
4648

4749

4850
@router.post(
49-
"/", dependencies=[Depends(get_current_active_superuser)], response_model=UserOut
51+
"/", dependencies=[Depends(get_current_active_superuser)], response_model=UserPublic
5052
)
5153
def create_user(*, session: SessionDep, user_in: UserCreate) -> Any:
5254
"""
@@ -72,7 +74,7 @@ def create_user(*, session: SessionDep, user_in: UserCreate) -> Any:
7274
return user
7375

7476

75-
@router.patch("/me", response_model=UserOut)
77+
@router.patch("/me", response_model=UserPublic)
7678
def update_user_me(
7779
*, session: SessionDep, user_in: UserUpdateMe, current_user: CurrentUser
7880
) -> Any:
@@ -114,15 +116,31 @@ def update_password_me(
114116
return Message(message="Password updated successfully")
115117

116118

117-
@router.get("/me", response_model=UserOut)
119+
@router.get("/me", response_model=UserPublic)
118120
def read_user_me(current_user: CurrentUser) -> Any:
119121
"""
120122
Get current user.
121123
"""
122124
return current_user
123125

124126

125-
@router.post("/signup", response_model=UserOut)
127+
@router.delete("/me", response_model=Message)
128+
def delete_user_me(session: SessionDep, current_user: CurrentUser) -> Any:
129+
"""
130+
Delete own user.
131+
"""
132+
if current_user.is_superuser:
133+
raise HTTPException(
134+
status_code=403, detail="Super users are not allowed to delete themselves"
135+
)
136+
statement = delete(Item).where(col(Item.owner_id) == current_user.id)
137+
session.exec(statement) # type: ignore
138+
session.delete(current_user)
139+
session.commit()
140+
return Message(message="User deleted successfully")
141+
142+
143+
@router.post("/signup", response_model=UserPublic)
126144
def register_user(session: SessionDep, user_in: UserRegister) -> Any:
127145
"""
128146
Create new user without the need to be logged in.
@@ -143,7 +161,7 @@ def register_user(session: SessionDep, user_in: UserRegister) -> Any:
143161
return user
144162

145163

146-
@router.get("/{user_id}", response_model=UserOut)
164+
@router.get("/{user_id}", response_model=UserPublic)
147165
def read_user_by_id(
148166
user_id: int, session: SessionDep, current_user: CurrentUser
149167
) -> Any:
@@ -164,7 +182,7 @@ def read_user_by_id(
164182
@router.patch(
165183
"/{user_id}",
166184
dependencies=[Depends(get_current_active_superuser)],
167-
response_model=UserOut,
185+
response_model=UserPublic,
168186
)
169187
def update_user(
170188
*,
@@ -193,7 +211,7 @@ def update_user(
193211
return db_user
194212

195213

196-
@router.delete("/{user_id}")
214+
@router.delete("/{user_id}", dependencies=[Depends(get_current_active_superuser)])
197215
def delete_user(
198216
session: SessionDep, current_user: CurrentUser, user_id: int
199217
) -> Message:
@@ -203,15 +221,10 @@ def delete_user(
203221
user = session.get(User, user_id)
204222
if not user:
205223
raise HTTPException(status_code=404, detail="User not found")
206-
elif user != current_user and not current_user.is_superuser:
207-
raise HTTPException(
208-
status_code=403, detail="The user doesn't have enough privileges"
209-
)
210-
elif user == current_user and current_user.is_superuser:
224+
if user == current_user:
211225
raise HTTPException(
212226
status_code=403, detail="Super users are not allowed to delete themselves"
213227
)
214-
215228
statement = delete(Item).where(col(Item.owner_id) == user_id)
216229
session.exec(statement) # type: ignore
217230
session.delete(user)

backend/app/clients/__init__.py

Whitespace-only changes.

backend/app/clients/ltd2.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from collections.abc import AsyncGenerator
2+
3+
from playwright.async_api import APIRequestContext, async_playwright
4+
5+
from app.core.config import settings
6+
from app.models import LTD2UnitFromAPI
7+
8+
assert settings.LTD2_X_API_KEY, "LTD2 X API KEY not set"
9+
10+
11+
async def get_api_request_context() -> AsyncGenerator[APIRequestContext, None]:
12+
async with async_playwright() as playwright:
13+
headers = {
14+
"x-api-key": settings.LTD2_X_API_KEY,
15+
}
16+
request_context = await playwright.request.new_context(
17+
base_url="https://apiv2.legiontd2.com", extra_http_headers=headers
18+
)
19+
yield request_context
20+
21+
22+
async def find_unit_by_name(name: str, api_request_context: APIRequestContext) -> dict: # type:ignore
23+
unit = await api_request_context.get(f"/units/byName/{name}")
24+
unit_response = await unit.json()
25+
return unit_response # type:ignore
26+
27+
28+
async def get_current_game_version(api_request_context: APIRequestContext) -> str:
29+
unit_response = await find_unit_by_name(
30+
name="Berserker", api_request_context=api_request_context
31+
)
32+
version = unit_response["version"]
33+
return version # type:ignore
34+
35+
36+
async def find_units_by_version(
37+
version: str, api_request_context: APIRequestContext
38+
) -> list[LTD2UnitFromAPI]:
39+
units = await api_request_context.get(
40+
f"/units/byVersion/{version}", params={"limit": 250}
41+
)
42+
units_response = await units.json()
43+
if len(units_response) == 250:
44+
units_remaining = await api_request_context.get(
45+
f"/units/byVersion/{version}", params={"limit": 250, "offset": 250}
46+
)
47+
units_remaining_response = await units_remaining.json()
48+
units_response = units_response + units_remaining_response
49+
50+
return [
51+
LTD2UnitFromAPI.model_validate(LTD2UnitFromAPI(**unit))
52+
for unit in units_response
53+
]

0 commit comments

Comments
 (0)