diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..293ecce --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +API_URI= +API_KEY= +API_TEAM= \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app.py b/app.py index 5d20a01..7d4fec4 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,9 @@ +from dotenv import load_dotenv from flask import Flask +from dto.requests import GameStartRequest +from services.julius_baer_api_client import JuliusBaerApiClient + app = Flask(__name__) @@ -9,4 +13,9 @@ def hello_world(): # put application's code here if __name__ == '__main__': + jb_client = JuliusBaerApiClient() + game_start_request = GameStartRequest("Welch") + res = jb_client.start_game(game_start_request) + print(res) + app.run() diff --git a/config.py b/config.py new file mode 100644 index 0000000..9e8a61a --- /dev/null +++ b/config.py @@ -0,0 +1,8 @@ +import os + +from dotenv import load_dotenv + +load_dotenv() +API_URI = str(os.getenv("API_URI") or "") +API_KEY = str(os.getenv("API_KEY") or "") +API_TEAM = str(os.getenv("API_TEAM") or "") \ No newline at end of file diff --git a/dto/__init__.py b/dto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dto/errors.py b/dto/errors.py new file mode 100644 index 0000000..e8d99f5 --- /dev/null +++ b/dto/errors.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass, field + + +@dataclass +class ValidationError: + """Model for validation errors.""" + loc: list + msg: str + type: str + + +@dataclass +class HTTPValidationError: + """Model for HTTP validation errors.""" + detail: list[ValidationError] = field(default_factory=list) diff --git a/dto/requests.py b/dto/requests.py new file mode 100644 index 0000000..3bfebab --- /dev/null +++ b/dto/requests.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass +from typing import Literal +from uuid import UUID + + +@dataclass +class GameStartRequest: + """Request model for starting a new game.""" + player_name: str + + +@dataclass +class GameDecisionRequest: + """Request model for making a game decision.""" + decision: Literal["Accept", "Reject"] + session_id: UUID + client_id: UUID diff --git a/dto/responses.py b/dto/responses.py new file mode 100644 index 0000000..1a4bfec --- /dev/null +++ b/dto/responses.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass +from typing import Dict, Optional, Any +from uuid import UUID + + +@dataclass +class GameStartResponse: + """Response model for a new game started.""" + message: str + session_id: UUID + player_id: str + client_id: UUID + client_data: Dict[str, Any] + score: int + + +@dataclass +class GameDecisionResponse: + """Response model for a game decision result.""" + status: str + score: int + client_id: Optional[UUID] = None + client_data: Optional[Dict[str, Any]] = None diff --git a/openapi.json b/openapi.json new file mode 100644 index 0000000..75fd0f8 --- /dev/null +++ b/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"Master","version":"0.1.0"},"paths":{"/game/start":{"post":{"summary":"Game Start","description":"Start a new game session for the player.","operationId":"game_start_game_start_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameStartRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameStartResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/game/decision":{"post":{"summary":"Game Decision","description":"Log decision, update the score and proceed with game.","operationId":"game_decision_game_decision_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameDecisionRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GameDecisionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"GameDecisionRequest":{"properties":{"decision":{"type":"string","enum":["Accept","Reject"],"title":"Decision","description":"Decision of the player.","example":"Accept"},"session_id":{"type":"string","format":"uuid","title":"Session Id","description":"Unique session ID for the game.","example":"6e8af95a-1819-4ac4-82b7-88846cf907cc"},"client_id":{"type":"string","format":"uuid","title":"Client Id","description":"Unique client ID for the game.","example":"2eb62743-b5e9-4225-b4b6-1ef4aaaf7813"}},"type":"object","required":["decision","session_id","client_id"],"title":"GameDecisionRequest"},"GameDecisionResponse":{"properties":{"status":{"type":"string","title":"Status","description":"Status of the game after the decision.","example":"gameover"},"score":{"type":"integer","title":"Score","description":"Current score of the player.","example":1},"client_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Client Id","description":"Unique client ID for the game.","example":"71737c82-8123-47a1-bc22-4ecf47ac5d7d"},"client_data":{"type":"object","title":"Client Data","description":"Client data for the player.","example":{"passport":"123456789"}}},"type":"object","required":["status","score"],"title":"GameDecisionResponse"},"GameStartRequest":{"properties":{"player_name":{"type":"string","title":"Player Name","description":"Name of the player.","example":"Haligali"}},"type":"object","required":["player_name"],"title":"GameStartRequest"},"GameStartResponse":{"properties":{"message":{"type":"string","title":"Message","description":"Message indicating the game has started.","example":"Game started successfully."},"session_id":{"type":"string","format":"uuid","title":"Session Id","description":"Unique session ID for the game.","example":"92ab2b1a-a3b5-4e36-af59-2d4083a18ee6"},"player_id":{"type":"string","title":"Player Id","description":"Unique player ID for the game.","example":"some_key"},"client_id":{"type":"string","format":"uuid","title":"Client Id","description":"Unique client ID for the game.","example":"42048bf6-5947-4797-bac9-348e23dcc904"},"client_data":{"type":"object","title":"Client Data","description":"Client data for the player.","example":{"data":{}}},"score":{"type":"integer","title":"Score","description":"Starting score of the player.","example":0}},"type":"object","required":["message","session_id","player_id","client_id","client_data","score"],"title":"GameStartResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}} \ No newline at end of file diff --git a/poc_requirements.txt b/poc_requirements.txt index c7e8e49..1f681d8 100644 --- a/poc_requirements.txt +++ b/poc_requirements.txt @@ -1,8 +1,25 @@ +annotated-types==0.7.0 +blinker==1.9.0 certifi==2025.1.31 charset-normalizer==3.4.1 +click==8.1.8 +Flask==3.1.0 idna==3.10 +itsdangerous==2.2.0 +Jinja2==3.1.6 +MarkupSafe==3.0.2 +openapi-client==1.1.7 packaging==24.2 pillow==11.1.0 +pydantic==2.11.3 +pydantic_core==2.33.1 pytesseract==0.3.13 +python-dateutil==2.9.0.post0 requests==2.32.3 +six==1.17.0 +typing-inspection==0.4.0 +typing_extensions==4.13.2 urllib3==2.4.0 +Werkzeug==3.1.3 +Flask==3.1.0 +python-dotenv=1.1.0 \ No newline at end of file diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/julius_baer_api_client.py b/services/julius_baer_api_client.py new file mode 100644 index 0000000..264f2c8 --- /dev/null +++ b/services/julius_baer_api_client.py @@ -0,0 +1,87 @@ +from typing import Dict, Any + +import requests + +import config +from dto.requests import GameStartRequest, GameDecisionRequest + + +class JuliusBaerApiClient: + """ + Client for interacting with the Julius Baer API service. + + Provides methods to start a game and make game decisions. + """ + + def __init__(self): + self.api_uri = config.API_URI + self.api_key = config.API_KEY + self.api_team = config.API_TEAM + + def start_game(self, game_start_request: GameStartRequest) -> Dict[str, Any]: + """ + Start a new game session. + + Args: + player_name: Name of the player. + + Returns: + Dict containing the game start response with session_id, player_id, etc. + """ + url = f"{self.api_uri}/game/start" + payload = {"player_name": game_start_request.player_name} + + headers = { + "x-api-key": self.api_key, + "Content-Type": "application/json" + } + + response = requests.post(url, json=payload, headers=headers) + response.raise_for_status() # Raise exception for HTTP errors + + data = response.json() + + # Store session_id and client_id for convenience in future calls + self.session_id = data.get("session_id") + self.client_id = data.get("client_id") + + return data + + def make_decision(self, game_decision_request: GameDecisionRequest) -> Dict[str, Any]: + """ + Make a game decision (Accept or Reject). + + Args: + decision: Either "Accept" or "Reject". + session_id: Unique session ID for the game. If None, uses the stored session_id. + client_id: Unique client ID for the game. If None, uses the stored client_id. + + Returns: + Dict containing the game decision response with status, score, etc. + + Raises: + ValueError: If decision is not "Accept" or "Reject". + ValueError: If session_id and client_id are not provided or stored from a previous start_game call. + """ + if game_decision_request.decision not in ["Accept", "Reject"]: + raise ValueError('Decision must be either "Accept" or "Reject"') + + # Use stored values if not provided + session_id = game_decision_request.session_id or self.session_id + client_id = game_decision_request.client_id or self.client_id + + if not session_id or not client_id: + raise ValueError( + "Session ID and Client ID are required. Either provide them explicitly or call start_game first.") + + url = f"{self.base_url}/game/decision" + payload = { + "decision": game_decision_request.decision, + "session_id": session_id, + "client_id": client_id + } + + response = requests.post(url, json=payload) + response.raise_for_status() # Raise exception for HTTP errors + + return response.json()