Merge remote-tracking branch 'origin/main'
This commit is contained in:
20
app.py
20
app.py
@ -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')
|
@ -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
|
@ -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
66
services/player.py
Normal 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()
|
||||
|
||||
|
||||
|
||||
|
||||
|
19
tests/test_cross_validate.py
Normal file
19
tests/test_cross_validate.py
Normal 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
0
utils/__init__.py
Normal file
36
validation/FromAccount.py
Normal file
36
validation/FromAccount.py
Normal 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
|
35
validation/FromDescription.py
Normal file
35
validation/FromDescription.py
Normal 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
|
||||
|
27
validation/FromPassport.py
Normal file
27
validation/FromPassport.py
Normal 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
65
validation/FromProfile.py
Normal 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
0
validation/__init__.py
Normal file
60
validation/cross_validate.py
Normal file
60
validation/cross_validate.py
Normal 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
|
Reference in New Issue
Block a user