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