Merge remote-tracking branch 'origin/main'

This commit is contained in:
robinrolle
2025-04-12 13:07:30 +02:00
12 changed files with 342 additions and 62 deletions

20
app.py
View File

@ -1,7 +1,10 @@
import logging
from flask import Flask from flask import Flask
from dto.requests import GameStartRequestDTO import config
from services.julius_baer_api_client import JuliusBaerApiClient from dto.requests import GameStartRequestDTO, GameDecisionRequestDTO
from services.player import Player
app = Flask(__name__) app = Flask(__name__)
@ -12,9 +15,14 @@ def hello_world(): # put application's code here
if __name__ == '__main__': if __name__ == '__main__':
jb_client = JuliusBaerApiClient()
game_start_request = GameStartRequestDTO(player_name="Welch") player = Player()
res = jb_client.start_game(game_start_request) player.play_on_separate_thread()
print(res)
app.run() 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')

View File

@ -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

View File

@ -1,7 +1,6 @@
import requests import requests
import config import config
import logging import logging
from typing import Dict, Any
from dto.requests import GameStartRequestDTO, GameDecisionRequestDTO from dto.requests import GameStartRequestDTO, GameDecisionRequestDTO
from dto.responses import GameStartResponseDTO, GameDecisionResponseDTO from dto.responses import GameStartResponseDTO, GameDecisionResponseDTO
@ -14,8 +13,6 @@ class JuliusBaerApiClient:
""" """
def __init__(self): def __init__(self):
self.client_id = None
self.session_id = None
self.api_uri = config.API_URI self.api_uri = config.API_URI
self.api_key = config.API_KEY self.api_key = config.API_KEY
self.api_team = config.API_TEAM self.api_team = config.API_TEAM
@ -33,63 +30,40 @@ class JuliusBaerApiClient:
Start a new game session. Start a new game session.
""" """
logging.info("[+] Starting 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 payload = game_start_request.model_dump_json()
try: try:
response = requests.post(start_url, json=payload, headers=self.headers) response = requests.post(start_uri, data=payload, headers=self.headers)
response.raise_for_status() # Raise exception for HTTP errors response.raise_for_status()
response_data = response.json() response_json = response.json()
validated_response = GameStartResponseDTO.model_validate(response_data) validated_response = GameStartResponseDTO.model_validate(response_json)
logging.info(f"Game started successfully. Session: {validated_response.session_id}, Client: {validated_response.client_id}") 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 return validated_response
except Exception as e: except Exception as e:
logging.error(f"[!] Failed to start game session: {e}") logging.error(f"[!] Failed to start game session: {e}")
raise 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). 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"]: logging.info("[+] Sending decision")
raise ValueError('Decision must be either "Accept" or "Reject"') decision_uri = f"{self.api_uri}/game/decision"
# Use stored values if not provided payload = game_decision_request.model_dump_json()
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: try:
raise ValueError( response = requests.post(decision_uri, data=payload, headers=self.headers)
"Session ID and Client ID are required. Either provide them explicitly or call start_game first.") response.raise_for_status()
url = f"{self.base_url}/game/decision" response_json = response.json()
payload = { validated_response = GameDecisionResponseDTO.model_validate(response_json)
"decision": game_decision_request.decision, logging.info("[+] Decision sent successfully")
"session_id": session_id,
"client_id": client_id
}
response = requests.post(url, json=payload) return validated_response
response.raise_for_status() # Raise exception for HTTP errors except Exception as e:
logging.error(f"[!] Failed to send a decision: {e}")
return response.json() raise

66
services/player.py Normal file
View File

@ -0,0 +1,66 @@
import logging
import threading
import time
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()
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 not in ['gameover', 'complete']:
payload = GameDecisionRequestDTO(
decision=decision,
session_id=start_response.session_id,
client_id=start_response.client_id,
)
decision_response = self.client.send_decision(payload)
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!
return 'Accept'
# import random
# return random.choice(["Accept", "Reject"])
if __name__ == '__main__':
player = Player()
player.start()

View File

@ -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"
)

0
utils/__init__.py Normal file
View File

36
validation/FromAccount.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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)

65
validation/FromProfile.py Normal file
View File

@ -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

0
validation/__init__.py Normal file
View File

View File

@ -0,0 +1,60 @@
from enum import StrEnum
from typing import Any, Callable, Optional
from pydantic import BaseModel
from validation import FromAccount, FromDescription, FromPassport, FromProfile
class ExtractedData(BaseModel):
account: FromAccount
description: FromDescription
passport: FromPassport
profile: FromProfile
class DocType(StrEnum):
account = "account"
description = "description"
passport = "passport"
profile = "profile"
class XValFailure(BaseModel):
doc1_type: DocType
doc1_val: str
doc2_type: DocType
doc2_val: str
def xval_name_account_description(data: ExtractedData) -> Optional[XValFailure]:
if data.account.account_holder_name != 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=}",
)
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:
failure = validator(data)
if not failure is None:
validation_failures.append(failure)
return validation_failures