From 9986e7409f8ca5e837b2a45ffecfaba98f43d293 Mon Sep 17 00:00:00 2001 From: NoeBerdoz Date: Sat, 12 Apr 2025 10:55:45 +0200 Subject: [PATCH 01/10] add empty init --- utils/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 utils/__init__.py diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 From 7969caacaf174868f0cabf7c0cdab3800d29ca36 Mon Sep 17 00:00:00 2001 From: dylan <12473240+dlmw@users.noreply.github.com> Date: Sat, 12 Apr 2025 11:37:08 +0200 Subject: [PATCH 02/10] Refactor make_decision and add usage example in app.py --- app.py | 16 ++++++-- services/julius_baer_api_client.py | 59 +++++++++--------------------- 2 files changed, 29 insertions(+), 46 deletions(-) diff --git a/app.py b/app.py index 6a0f4c1..0adc89a 100644 --- a/app.py +++ b/app.py @@ -1,6 +1,5 @@ from flask import Flask -from dto.requests import GameStartRequestDTO from services.julius_baer_api_client import JuliusBaerApiClient app = Flask(__name__) @@ -13,8 +12,17 @@ def hello_world(): # put application's code here if __name__ == '__main__': jb_client = JuliusBaerApiClient() - game_start_request = GameStartRequestDTO(player_name="Welch") - res = jb_client.start_game(game_start_request) - print(res) + # game_start_request = GameStartRequestDTO(player_name=config.API_TEAM) + # res = jb_client.start_game(game_start_request) + # + # game_decision_request = GameDecisionRequestDTO(decision="Accept", client_id=res.client_id, session_id=res.session_id) + # decision_response = jb_client.make_decision(game_decision_request) + # + # while decision_response.status == "active": + # game_decision_request = GameDecisionRequestDTO(decision="Accept", client_id=res.client_id, session_id=res.session_id) + # decision_response = jb_client.make_decision(game_decision_request) + # + # if decision_response.status == "gameover": + # logging.info("Game over") app.run() diff --git a/services/julius_baer_api_client.py b/services/julius_baer_api_client.py index b19db6e..a61315e 100644 --- a/services/julius_baer_api_client.py +++ b/services/julius_baer_api_client.py @@ -14,8 +14,6 @@ class JuliusBaerApiClient: """ def __init__(self): - self.client_id = None - self.session_id = None self.api_uri = config.API_URI self.api_key = config.API_KEY self.api_team = config.API_TEAM @@ -33,24 +31,18 @@ class JuliusBaerApiClient: Start a new game session. """ logging.info("[+] Starting new game session") - start_url = f"{self.api_uri}/game/start" + start_uri = f"{self.api_uri}/game/start" payload = game_start_request.model_dump() # Convert GameStartRequestDTO to dict for JSON try: - response = requests.post(start_url, json=payload, headers=self.headers) + response = requests.post(start_uri, json=payload, headers=self.headers) response.raise_for_status() # Raise exception for HTTP errors - response_data = response.json() - validated_response = GameStartResponseDTO.model_validate(response_data) + response_json = response.json() + validated_response = GameStartResponseDTO.model_validate(response_json) logging.info(f"Game started successfully. Session: {validated_response.session_id}, Client: {validated_response.client_id}") - - # Store session_id and client_id for future calls - self.session_id = validated_response.session_id - self.client_id = validated_response.client_id - return validated_response - except Exception as e: logging.error(f"[!] Failed to start game session: {e}") raise @@ -58,38 +50,21 @@ class JuliusBaerApiClient: def make_decision(self, game_decision_request: GameDecisionRequestDTO) -> GameDecisionResponseDTO: """ 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"') + logging.info("[+] Making decision") + decision_uri = f"{self.api_uri}/game/decision" - # 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 + payload = game_decision_request.model_dump_json() - 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.") + try: + response = requests.post(decision_uri, headers=self.headers, data=payload) + response.raise_for_status() - url = f"{self.base_url}/game/decision" - payload = { - "decision": game_decision_request.decision, - "session_id": session_id, - "client_id": client_id - } + response_json = response.json() + validated_response = GameDecisionResponseDTO.model_validate(response_json) + logging.info("Game decision made successfully") - response = requests.post(url, json=payload) - response.raise_for_status() # Raise exception for HTTP errors - - return response.json() + return validated_response + except Exception as e: + logging.error(f"[!] Failed to start game session: {e}") + raise From a1c4ef0f22ff2696f9c135d3bf8f757e5f1352a8 Mon Sep 17 00:00:00 2001 From: dylan <12473240+dlmw@users.noreply.github.com> Date: Sat, 12 Apr 2025 11:44:50 +0200 Subject: [PATCH 03/10] Make start_game similar to make_decision --- services/julius_baer_api_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/julius_baer_api_client.py b/services/julius_baer_api_client.py index a61315e..f7313f3 100644 --- a/services/julius_baer_api_client.py +++ b/services/julius_baer_api_client.py @@ -32,11 +32,11 @@ class JuliusBaerApiClient: """ logging.info("[+] Starting new game session") start_uri = f"{self.api_uri}/game/start" - payload = game_start_request.model_dump() # Convert GameStartRequestDTO to dict for JSON + payload = game_start_request.model_dump_json() try: - response = requests.post(start_uri, json=payload, headers=self.headers) - response.raise_for_status() # Raise exception for HTTP errors + response = requests.post(start_uri, data=payload, headers=self.headers) + response.raise_for_status() response_json = response.json() validated_response = GameStartResponseDTO.model_validate(response_json) @@ -57,7 +57,7 @@ class JuliusBaerApiClient: payload = game_decision_request.model_dump_json() try: - response = requests.post(decision_uri, headers=self.headers, data=payload) + response = requests.post(decision_uri, data=payload, headers=self.headers) response.raise_for_status() response_json = response.json() From 0929fc8c333d354a7c6ce190dbdaaa6ef9fc23cc Mon Sep 17 00:00:00 2001 From: NoeBerdoz Date: Sat, 12 Apr 2025 11:51:09 +0200 Subject: [PATCH 04/10] minor refactor on loggings --- services/julius_baer_api_client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/services/julius_baer_api_client.py b/services/julius_baer_api_client.py index a61315e..667d3b9 100644 --- a/services/julius_baer_api_client.py +++ b/services/julius_baer_api_client.py @@ -1,7 +1,6 @@ import requests import config import logging -from typing import Dict, Any from dto.requests import GameStartRequestDTO, GameDecisionRequestDTO from dto.responses import GameStartResponseDTO, GameDecisionResponseDTO @@ -51,7 +50,7 @@ class JuliusBaerApiClient: """ Make a game decision (Accept or Reject). """ - logging.info("[+] Making decision") + logging.info("[+] Sending decision") decision_uri = f"{self.api_uri}/game/decision" payload = game_decision_request.model_dump_json() @@ -62,9 +61,9 @@ class JuliusBaerApiClient: response_json = response.json() validated_response = GameDecisionResponseDTO.model_validate(response_json) - logging.info("Game decision made successfully") + logging.info("[+] Decision sent successfully") return validated_response except Exception as e: - logging.error(f"[!] Failed to start game session: {e}") + logging.error(f"[!] Failed to send a decision: {e}") raise From e96549604c2b4664ca228099d2f0123ee76e6ae0 Mon Sep 17 00:00:00 2001 From: Nitwix Date: Sat, 12 Apr 2025 11:55:18 +0200 Subject: [PATCH 05/10] Skeleton for final data validation and xref --- dto/client_data.py | 10 ----- dto/client_data/FromAccount.py | 36 +++++++++++++++++ dto/client_data/FromDescription.py | 35 ++++++++++++++++ dto/client_data/FromPassport.py | 27 +++++++++++++ dto/client_data/FromProfile.py | 65 ++++++++++++++++++++++++++++++ dto/client_data/__init__.py | 0 dto/client_data/cross_validate.py | 44 ++++++++++++++++++++ 7 files changed, 207 insertions(+), 10 deletions(-) delete mode 100644 dto/client_data.py create mode 100644 dto/client_data/FromAccount.py create mode 100644 dto/client_data/FromDescription.py create mode 100644 dto/client_data/FromPassport.py create mode 100644 dto/client_data/FromProfile.py create mode 100644 dto/client_data/__init__.py create mode 100644 dto/client_data/cross_validate.py diff --git a/dto/client_data.py b/dto/client_data.py deleted file mode 100644 index e47274f..0000000 --- a/dto/client_data.py +++ /dev/null @@ -1,10 +0,0 @@ -from pydantic import BaseModel - -class ClientData(BaseModel): - """ - Model for the client data attributes which need to be validated and compared for correspondence between - the data sources () - """ - name: str - - # TODO CONTINUE \ No newline at end of file diff --git a/dto/client_data/FromAccount.py b/dto/client_data/FromAccount.py new file mode 100644 index 0000000..91307c8 --- /dev/null +++ b/dto/client_data/FromAccount.py @@ -0,0 +1,36 @@ +from typing import Literal, Optional, Self +from pydantic import BaseModel, ConfigDict, EmailStr, Field, model_validator + + +class FromAccount(BaseModel): + """ + Fields which can be extracted from account.pdf + """ + model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True) + + # From account.pdf + account_name: str = Field(..., min_length=1) + account_holder_name: str = Field(..., min_length=1) + account_holder_surname: str = Field(..., min_length=1) + + @model_validator(mode='after') + def check_account_name_is_name_surname(self) -> Self: + combined = f"{self.account_holder_name} {self.account_holder_surname}" + if combined != self.account_name: + raise ValueError(f'Account name is not name + surname: {self.account_name} != {combined}') + return self + + passport_number: str = Field(..., min_length=5) + + reference_currency: Literal["CHF", "EUR", "USD", "Other"] + other_currency: Optional[str] = None + + building_number: str = Field(..., min_length=1) + street_name: str = Field(..., min_length=1) + postal_code: str = Field(..., min_length=1) + city: str = Field(..., min_length=1) + country: str = Field(..., min_length=1) + + name: str = Field(..., min_length=1) + phone_number: str = Field(..., min_length=6) + email: EmailStr \ No newline at end of file diff --git a/dto/client_data/FromDescription.py b/dto/client_data/FromDescription.py new file mode 100644 index 0000000..e19420e --- /dev/null +++ b/dto/client_data/FromDescription.py @@ -0,0 +1,35 @@ +from typing import Literal, Optional +from pydantic import BaseModel, ConfigDict, EmailStr, Field + + +class FromDescription(BaseModel): + """ + Fields which can be extracted from description.txt + """ + model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True) + + + full_name: str = Field(..., min_length=1) + age: int = Field(..., ge=0, le=120) + nationality: str = Field(..., min_length=1) + + marital_status: Literal["single", "married", "divorced", "widowed"] + has_children: bool + + secondary_education_school: str + secondary_education_year: int = Field(..., ge=1900, le=2100) + university_name: str + university_graduation_year: int = Field(..., ge=1900, le=2100) + + occupation_title: str + employer: str + start_year: int = Field(..., ge=1900, le=2100) + annual_salary_eur: float = Field(..., ge=0) + + total_savings_eur: float = Field(..., ge=0) + has_properties: bool + + inheritance_amount_eur: float = Field(..., ge=0) + inheritance_year: int = Field(..., ge=1900, le=2100) + inheritance_source: str + diff --git a/dto/client_data/FromPassport.py b/dto/client_data/FromPassport.py new file mode 100644 index 0000000..773d135 --- /dev/null +++ b/dto/client_data/FromPassport.py @@ -0,0 +1,27 @@ +from datetime import date +from typing import Literal +from pydantic import BaseModel, ConfigDict, Field + + +class FromPassport(BaseModel): + """ + Fields which can be extracted from description.txt + """ + model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True) + + country: str = Field(..., min_length=3, max_length=3) # ISO 3166-1 alpha-3 + passport_number: str = Field(..., min_length=9, max_length=9, regex=r"^[A-Z0-9]{9}$") + + surname: str = Field(..., min_length=1) + given_names: str = Field(..., min_length=1) + + birth_date: date + citizenship: str = Field(..., min_length=2) + sex: Literal["M", "F"] + + issue_date: date + expiry_date: date + + signature_present: bool + + machine_readable_zone: str = Field(..., min_length=44) \ No newline at end of file diff --git a/dto/client_data/FromProfile.py b/dto/client_data/FromProfile.py new file mode 100644 index 0000000..64abbee --- /dev/null +++ b/dto/client_data/FromProfile.py @@ -0,0 +1,65 @@ +from datetime import date +from typing import List, Literal, Optional +from pydantic import BaseModel, ConfigDict, EmailStr, Field + + +class FromProfile(BaseModel): + """ + Fields which can be extracted from description.txt + """ + model_config = ConfigDict(validate_assignment=True, str_strip_whitespace=True) + + first_name: str = Field(..., min_length=1) + last_name: str = Field(..., min_length=1) + date_of_birth: date + nationality: str + country_of_domicile: str + gender: Literal["Female", "Male"] + + # ID information + passport_number: str = Field(..., min_length=9, max_length=9, regex=r"^[A-Z0-9]{9}$") + id_type: Literal["passport"] + id_issue_date: date + id_expiry_date: date + + # Contact + phone: str = Field(..., min_length=8) + email: EmailStr + address: str + + # Personal info + politically_exposed_person: bool + marital_status: Literal["Single", "Married", "Divorced", "Widowed"] + highest_education: Literal["Tertiary", "Secondary", "Primary", "None"] + education_history: Optional[str] = None + + # Employment + employment_status: Literal["Employee", "Self-Employed", "Unemployed", "Retired", "Student", "Diplomat", "Military", "Homemaker", "Other"] + employment_since: Optional[int] = None + employer: Optional[str] = None + position: Optional[str] = None + annual_salary_eur: Optional[float] = None + + # Wealth background + total_wealth_range: Literal["<1.5m", "1.5m-5m", "5m-10m", "10m-20m", "20m-50m", ">50m"] + origin_of_wealth: List[Literal["Employment", "Inheritance", "Business", "Investments", "Sale of real estate", "Retirement package", "Other"]] + inheritance_details: Optional[str] = None + + # Assets + business_assets_eur: float = Field(..., ge=0) + + # Income + estimated_annual_income: Literal["<250k", "250k-500k", "500k-1m", ">1m"] + income_country: str + + # Account preferences + commercial_account: bool + investment_risk_profile: Literal["Low", "Moderate", "Considerable", "High"] + mandate_type: Literal["Advisory", "Discretionary"] + investment_experience: Literal["Inexperienced", "Experienced", "Expert"] + investment_horizon: Literal["Short", "Medium", "Long-Term"] + preferred_markets: List[str] + + # Assets under management + total_aum: float + aum_to_transfer: float \ No newline at end of file diff --git a/dto/client_data/__init__.py b/dto/client_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dto/client_data/cross_validate.py b/dto/client_data/cross_validate.py new file mode 100644 index 0000000..e545e40 --- /dev/null +++ b/dto/client_data/cross_validate.py @@ -0,0 +1,44 @@ +from enum import StrEnum +from typing import Any, Callable +from dto.client_data import FromDescription, FromPassport, FromProfile +from dto.client_data.FromAccount import FromAccount +from pydantic import BaseModel + + +class ValidatedData(BaseModel): + account: FromAccount + description: FromDescription + passport: FromPassport + profile: FromProfile + +class DocType(StrEnum): + account = "account" + description = "description" + passport = "passport" + profile = "profile" + + +class ValidationFailure(BaseModel): + doc1_type: DocType + doc1_val: str + + doc2_type: DocType + doc2_val: str + + + +def xref_client_name(data: ValidatedData) -> ValidationFailure: + if data.account.account_holder_name != data.description.full_name: + return ValidationFailure( + doc1_type=DocType.account, doc1_val=f"{data.account.account_holder_name=}", + doc2_type=DocType.description, doc2_val=f"{data.description.full_name=}" + ) + # TODO CONTINUE + +def xref_all(data: ValidatedData) -> list[ValidationFailure]: + xref_validators: list[Callable[[ValidatedData], ValidationFailure]] = [xref_client_name] + + validation_failures = [] + for validator in xref_validators: + validation_failures.append(validator(data)) + return validation_failures \ No newline at end of file From e176892dab522e0d6201ac4283072cdfeec97041 Mon Sep 17 00:00:00 2001 From: Luca De Laurentiis <39311040+lucadela96@users.noreply.github.com> Date: Sat, 12 Apr 2025 11:58:01 +0200 Subject: [PATCH 06/10] Player (#1) * Added player --- services/player.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 services/player.py diff --git a/services/player.py b/services/player.py new file mode 100644 index 0000000..89c68a1 --- /dev/null +++ b/services/player.py @@ -0,0 +1,53 @@ +import logging +from typing import Literal, Dict, Any +import config +from dto.requests import GameStartRequestDTO, GameDecisionRequestDTO +from services.julius_baer_api_client import JuliusBaerApiClient + + +class Player: + + def __init__(self): + self.client = JuliusBaerApiClient() + + def start(self): + self.play() + + def play(self): + payload = GameStartRequestDTO(player_name=config.API_TEAM) + start_response = self.client.start_game(payload) + logging.info(start_response) + + status = '' + decision = self.make_decision(start_response.client_data) + while status != 'gameover': + + payload = GameDecisionRequestDTO( + decision=decision, + session_id=start_response.session_id, + client_id=start_response.client_id, + ) + + decision_response = self.client.make_decision(payload) + logging.info(decision_response) + status = decision_response.status + decision = self.make_decision(decision_response.client_data) + + + def make_decision(self, client_data: Dict[str, Any]) -> Literal["Accept", "Reject"]: + # Do your magic! + + return 'Accept' + + # import random + # return random.choice(["Accept", "Reject"]) + + +if __name__ == '__main__': + player = Player() + player.start() + + + + + From e96af14f6c9606f7bdbdca9261b76d6fe8102390 Mon Sep 17 00:00:00 2001 From: Nitwix Date: Sat, 12 Apr 2025 12:01:02 +0200 Subject: [PATCH 07/10] Refactor validation into validation/ --- {dto/client_data => validation}/FromAccount.py | 0 {dto/client_data => validation}/FromDescription.py | 0 {dto/client_data => validation}/FromPassport.py | 0 {dto/client_data => validation}/FromProfile.py | 0 {dto/client_data => validation}/__init__.py | 0 {dto/client_data => validation}/cross_validate.py | 5 +++-- 6 files changed, 3 insertions(+), 2 deletions(-) rename {dto/client_data => validation}/FromAccount.py (100%) rename {dto/client_data => validation}/FromDescription.py (100%) rename {dto/client_data => validation}/FromPassport.py (100%) rename {dto/client_data => validation}/FromProfile.py (100%) rename {dto/client_data => validation}/__init__.py (100%) rename {dto/client_data => validation}/cross_validate.py (90%) diff --git a/dto/client_data/FromAccount.py b/validation/FromAccount.py similarity index 100% rename from dto/client_data/FromAccount.py rename to validation/FromAccount.py diff --git a/dto/client_data/FromDescription.py b/validation/FromDescription.py similarity index 100% rename from dto/client_data/FromDescription.py rename to validation/FromDescription.py diff --git a/dto/client_data/FromPassport.py b/validation/FromPassport.py similarity index 100% rename from dto/client_data/FromPassport.py rename to validation/FromPassport.py diff --git a/dto/client_data/FromProfile.py b/validation/FromProfile.py similarity index 100% rename from dto/client_data/FromProfile.py rename to validation/FromProfile.py diff --git a/dto/client_data/__init__.py b/validation/__init__.py similarity index 100% rename from dto/client_data/__init__.py rename to validation/__init__.py diff --git a/dto/client_data/cross_validate.py b/validation/cross_validate.py similarity index 90% rename from dto/client_data/cross_validate.py rename to validation/cross_validate.py index e545e40..c07ce14 100644 --- a/dto/client_data/cross_validate.py +++ b/validation/cross_validate.py @@ -1,9 +1,10 @@ from enum import StrEnum from typing import Any, Callable -from dto.client_data import FromDescription, FromPassport, FromProfile -from dto.client_data.FromAccount import FromAccount from pydantic import BaseModel +from validation import FromAccount, FromDescription, FromPassport, FromProfile + + class ValidatedData(BaseModel): account: FromAccount From 321217db6030e80e083139948b94a6e7f6998e2e Mon Sep 17 00:00:00 2001 From: NoeBerdoz Date: Sat, 12 Apr 2025 12:51:13 +0200 Subject: [PATCH 08/10] rename make_decision method to send_decision --- app.py | 14 ++++++++++++-- services/julius_baer_api_client.py | 2 +- services/player.py | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app.py b/app.py index 0adc89a..fef0bdb 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,9 @@ +import logging + from flask import Flask +import config +from dto.requests import GameStartRequestDTO, GameDecisionRequestDTO from services.julius_baer_api_client import JuliusBaerApiClient app = Flask(__name__) @@ -16,13 +20,19 @@ if __name__ == '__main__': # res = jb_client.start_game(game_start_request) # # game_decision_request = GameDecisionRequestDTO(decision="Accept", client_id=res.client_id, session_id=res.session_id) - # decision_response = jb_client.make_decision(game_decision_request) + # decision_response = jb_client.send_decision(game_decision_request) # # while decision_response.status == "active": # game_decision_request = GameDecisionRequestDTO(decision="Accept", client_id=res.client_id, session_id=res.session_id) - # decision_response = jb_client.make_decision(game_decision_request) + # decision_response = jb_client.send_decision(game_decision_request) # # if decision_response.status == "gameover": # logging.info("Game over") app.run() + + # res.session_id + # UUID('fde19363-a3d5-432e-8b87-54a6dd54f0dd') + # second test UUID('e3d58302-400a-4bc6-9772-ae50de43c9f4') + # UUID('f8b2a0a6-d4e0-45e6-900f-8ecb3c28f993') + # UUID('f8b2a0a6-d4e0-45e6-900f-8ecb3c28f993') \ No newline at end of file diff --git a/services/julius_baer_api_client.py b/services/julius_baer_api_client.py index fcd093a..d8b5f0d 100644 --- a/services/julius_baer_api_client.py +++ b/services/julius_baer_api_client.py @@ -46,7 +46,7 @@ class JuliusBaerApiClient: logging.error(f"[!] Failed to start game session: {e}") raise - def make_decision(self, game_decision_request: GameDecisionRequestDTO) -> GameDecisionResponseDTO: + def send_decision(self, game_decision_request: GameDecisionRequestDTO) -> GameDecisionResponseDTO: """ Make a game decision (Accept or Reject). """ diff --git a/services/player.py b/services/player.py index 89c68a1..3b24805 100644 --- a/services/player.py +++ b/services/player.py @@ -28,7 +28,7 @@ class Player: client_id=start_response.client_id, ) - decision_response = self.client.make_decision(payload) + decision_response = self.client.send_decision(payload) logging.info(decision_response) status = decision_response.status decision = self.make_decision(decision_response.client_data) From 92c86a48b6b27ed2d630906c664b54fc8a90c1c5 Mon Sep 17 00:00:00 2001 From: luca Date: Sat, 12 Apr 2025 12:59:36 +0200 Subject: [PATCH 09/10] Player async function and integration with app.py --- app.py | 18 ++++-------------- services/player.py | 19 ++++++++++++++++--- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app.py b/app.py index fef0bdb..1623c19 100644 --- a/app.py +++ b/app.py @@ -4,7 +4,7 @@ from flask import Flask import config from dto.requests import GameStartRequestDTO, GameDecisionRequestDTO -from services.julius_baer_api_client import JuliusBaerApiClient +from services.player import Player app = Flask(__name__) @@ -15,19 +15,9 @@ def hello_world(): # put application's code here if __name__ == '__main__': - jb_client = JuliusBaerApiClient() - # game_start_request = GameStartRequestDTO(player_name=config.API_TEAM) - # res = jb_client.start_game(game_start_request) - # - # game_decision_request = GameDecisionRequestDTO(decision="Accept", client_id=res.client_id, session_id=res.session_id) - # decision_response = jb_client.send_decision(game_decision_request) - # - # while decision_response.status == "active": - # game_decision_request = GameDecisionRequestDTO(decision="Accept", client_id=res.client_id, session_id=res.session_id) - # decision_response = jb_client.send_decision(game_decision_request) - # - # if decision_response.status == "gameover": - # logging.info("Game over") + + player = Player() + player.play_on_separate_thread() app.run() diff --git a/services/player.py b/services/player.py index 3b24805..3cae73f 100644 --- a/services/player.py +++ b/services/player.py @@ -1,4 +1,6 @@ import logging +import threading +import time from typing import Literal, Dict, Any import config from dto.requests import GameStartRequestDTO, GameDecisionRequestDTO @@ -9,18 +11,29 @@ class Player: def __init__(self): self.client = JuliusBaerApiClient() + self._thread = None def start(self): self.play() + def play_on_separate_thread(self): + if self._thread and self._thread.is_alive(): + logging.warning('Game loop already running.') + return self._thread + + self._thread = threading.Thread(target=self.play, daemon=True) + self._thread.start() + return self._thread + def play(self): + print('playing') payload = GameStartRequestDTO(player_name=config.API_TEAM) start_response = self.client.start_game(payload) logging.info(start_response) status = '' decision = self.make_decision(start_response.client_data) - while status != 'gameover': + while status not in ['gameover', 'complete']: payload = GameDecisionRequestDTO( decision=decision, @@ -29,10 +42,10 @@ class Player: ) decision_response = self.client.send_decision(payload) - logging.info(decision_response) + print(decision_response.status, decision_response.score) status = decision_response.status decision = self.make_decision(decision_response.client_data) - + time.sleep(1.5) def make_decision(self, client_data: Dict[str, Any]) -> Literal["Accept", "Reject"]: # Do your magic! From a3b3e957b74f10e85e738bdeb389c6379c2c2001 Mon Sep 17 00:00:00 2001 From: Nitwix Date: Sat, 12 Apr 2025 13:04:26 +0200 Subject: [PATCH 10/10] Impl cross_validate in progress --- tests/test_cross_validate.py | 19 ++++++++++++++++ validation/cross_validate.py | 43 ++++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 tests/test_cross_validate.py diff --git a/tests/test_cross_validate.py b/tests/test_cross_validate.py new file mode 100644 index 0000000..9457c6e --- /dev/null +++ b/tests/test_cross_validate.py @@ -0,0 +1,19 @@ +from validation import FromAccount + + +account_data = FromAccount( + account_name="Astrid Janneke Willems", + account_holder_name="Astrid Janneke", + account_holder_surname="Willems", + passport_number="HW8642009", + reference_currency="EUR", + other_currency=None, + building_number="18", + street_name="Lijnbaan", + postal_code="7523 05", + city="Assen", + country="Netherlands", + name="Astrid Janneke Willems", + phone_number="+31 06 34579996", + email="astrid.willems@upcmail.nl" +) \ No newline at end of file diff --git a/validation/cross_validate.py b/validation/cross_validate.py index c07ce14..928c460 100644 --- a/validation/cross_validate.py +++ b/validation/cross_validate.py @@ -1,17 +1,17 @@ from enum import StrEnum -from typing import Any, Callable +from typing import Any, Callable, Optional from pydantic import BaseModel from validation import FromAccount, FromDescription, FromPassport, FromProfile - -class ValidatedData(BaseModel): +class ExtractedData(BaseModel): account: FromAccount description: FromDescription passport: FromPassport profile: FromProfile + class DocType(StrEnum): account = "account" description = "description" @@ -19,7 +19,7 @@ class DocType(StrEnum): profile = "profile" -class ValidationFailure(BaseModel): +class XValFailure(BaseModel): doc1_type: DocType doc1_val: str @@ -27,19 +27,34 @@ class ValidationFailure(BaseModel): doc2_val: str - -def xref_client_name(data: ValidatedData) -> ValidationFailure: +def xval_name_account_description(data: ExtractedData) -> Optional[XValFailure]: if data.account.account_holder_name != data.description.full_name: - return ValidationFailure( - doc1_type=DocType.account, doc1_val=f"{data.account.account_holder_name=}", - doc2_type=DocType.description, doc2_val=f"{data.description.full_name=}" + return XValFailure( + doc1_type=DocType.account, + doc1_val=f"{data.account.account_holder_name=}", + doc2_type=DocType.description, + doc2_val=f"{data.description.full_name=}", ) - # TODO CONTINUE -def xref_all(data: ValidatedData) -> list[ValidationFailure]: - xref_validators: list[Callable[[ValidatedData], ValidationFailure]] = [xref_client_name] + +def xval_email_account_profile(data: ExtractedData) -> Optional[XValFailure]: + if data.account.email != data.profile.email: + return XValFailure( + doc1_type=DocType.account, + doc1_val=f"{data.account.email=}", + doc2_type=DocType.profile, + doc2_val=f"{data.profile.email=}" + ) + + +def xref_all(data: ExtractedData) -> list[XValFailure]: + xref_validators: list[Callable[[ExtractedData], Optional[XValFailure]]] = [ + xval_name_account_description + ] validation_failures = [] for validator in xref_validators: - validation_failures.append(validator(data)) - return validation_failures \ No newline at end of file + failure = validator(data) + if not failure is None: + validation_failures.append(failure) + return validation_failures