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 dto.requests import GameStartRequestDTO
from services.julius_baer_api_client import JuliusBaerApiClient
import config
from dto.requests import GameStartRequestDTO, GameDecisionRequestDTO
from services.player import Player
app = Flask(__name__)
@ -12,9 +15,14 @@ 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)
player = Player()
player.play_on_separate_thread()
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 config
import logging
from typing import Dict, Any
from dto.requests import GameStartRequestDTO, GameDecisionRequestDTO
from dto.responses import GameStartResponseDTO, GameDecisionResponseDTO
@ -14,8 +13,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,63 +30,40 @@ class JuliusBaerApiClient:
Start a new game session.
"""
logging.info("[+] Starting new game session")
start_url = f"{self.api_uri}/game/start"
payload = game_start_request.model_dump() # Convert GameStartRequestDTO to dict for JSON
start_uri = f"{self.api_uri}/game/start"
payload = game_start_request.model_dump_json()
try:
response = requests.post(start_url, 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_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
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).
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("[+] Sending 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, data=payload, headers=self.headers)
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("[+] Decision sent 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 send a decision: {e}")
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