game-api-client 0.1.0__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: game-api-client
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Python client library for GAME trading API
5
5
  License: MIT
6
6
  Keywords: api,trading,game,client
@@ -18,6 +18,7 @@ Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Programming Language :: Python :: 3.13
20
20
  Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Programming Language :: Python :: 3.15
21
22
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
23
  Requires-Dist: httpx (>=0.28.1,<0.29.0)
23
24
  Requires-Dist: pydantic (>=2.10.0,<3.0.0)
@@ -131,6 +132,8 @@ GAME_API_VERIFY_SSL=false
131
132
  - `market_zone`: Market zone like "10YCH-SWISSGRIDZ" (optional)
132
133
  - `get_latest_ltp()` - Get latest LTP per contract across all zones
133
134
  - Returns dict mapping `contract_id -> {price, volume, timestamp, market_zones, latest_zone}`
135
+ - `get_latest_available_ltp(duration=None)` - Get the newest LTP compatible with available cross-border capacity
136
+ - Falls back to the next eligible trade and uses the Swiss LTP when no cross-border capacity is available
134
137
 
135
138
  ### Order Management
136
139
  - `get_orders(direction=None)` - Get orders with optional filter
@@ -143,7 +146,10 @@ GAME_API_VERIFY_SSL=false
143
146
  - `order_id`: Order ID (handles both string and numeric formats)
144
147
 
145
148
  ### Spot Data
146
- - `get_spot_data()` - Get spot market data
149
+ - `get_spot_data(area_name=None, date_from=None, date_to=None, limit=None)` - Get spot market data
150
+ - `area_name`: Area name, such as `CH-SGD` (optional; API default: `CH-SGD`)
151
+ - `date_from`, `date_to`: Date range in `YYYY-MM-DD` format (optional)
152
+ - `limit`: Maximum number of results (optional; API default: `500`)
147
153
  - `get_spot_status()` - Get spot API status
148
154
 
149
155
  ## Usage Examples
@@ -103,6 +103,8 @@ GAME_API_VERIFY_SSL=false
103
103
  - `market_zone`: Market zone like "10YCH-SWISSGRIDZ" (optional)
104
104
  - `get_latest_ltp()` - Get latest LTP per contract across all zones
105
105
  - Returns dict mapping `contract_id -> {price, volume, timestamp, market_zones, latest_zone}`
106
+ - `get_latest_available_ltp(duration=None)` - Get the newest LTP compatible with available cross-border capacity
107
+ - Falls back to the next eligible trade and uses the Swiss LTP when no cross-border capacity is available
106
108
 
107
109
  ### Order Management
108
110
  - `get_orders(direction=None)` - Get orders with optional filter
@@ -115,7 +117,10 @@ GAME_API_VERIFY_SSL=false
115
117
  - `order_id`: Order ID (handles both string and numeric formats)
116
118
 
117
119
  ### Spot Data
118
- - `get_spot_data()` - Get spot market data
120
+ - `get_spot_data(area_name=None, date_from=None, date_to=None, limit=None)` - Get spot market data
121
+ - `area_name`: Area name, such as `CH-SGD` (optional; API default: `CH-SGD`)
122
+ - `date_from`, `date_to`: Date range in `YYYY-MM-DD` format (optional)
123
+ - `limit`: Maximum number of results (optional; API default: `500`)
119
124
  - `get_spot_status()` - Get spot API status
120
125
 
121
126
  ## Usage Examples
@@ -4,10 +4,12 @@ Simple GAME API Client
4
4
  A minimal client for the GAME trading API.
5
5
  """
6
6
 
7
+ from __future__ import annotations
8
+
7
9
  import logging
8
10
  import time
9
- from datetime import datetime, timezone
10
- from typing import Any, Dict, List, Optional
11
+ from datetime import datetime
12
+ from typing import Any
11
13
 
12
14
  import httpx
13
15
 
@@ -17,8 +19,6 @@ logger = logging.getLogger(__name__)
17
19
  class GameAPIError(Exception):
18
20
  """Base exception for GAME API errors"""
19
21
 
20
- pass
21
-
22
22
 
23
23
  # Valid order states
24
24
  VALID_ORDER_STATES = ["displayed", "hidden"]
@@ -31,9 +31,8 @@ def get_default_tz_offset_hours() -> int:
31
31
  Mirrors the frontend's getCurrentTimezoneOffset() logic.
32
32
  Returns 1 for CET, 2 for CEST, 0 for UTC.
33
33
  """
34
- now = datetime.now()
35
- utc_now = datetime.now(timezone.utc)
36
- offset_seconds = (now - utc_now.replace(tzinfo=None)).total_seconds()
34
+ local_offset = datetime.now().astimezone().utcoffset()
35
+ offset_seconds = local_offset.total_seconds() if local_offset else 0
37
36
  offset_hours = round(offset_seconds / 3600)
38
37
  return max(0, min(2, offset_hours))
39
38
 
@@ -62,8 +61,8 @@ class GameAPI:
62
61
  self.verify_ssl = verify_ssl
63
62
  self.timeout = timeout
64
63
  self.personal_token = token
65
- self._access_token: Optional[str] = None
66
- self._token_expires_at: Optional[float] = None
64
+ self._access_token: str | None = None
65
+ self._token_expires_at: float | None = None
67
66
 
68
67
  if not token:
69
68
  raise GameAPIError("Personal token is required")
@@ -116,8 +115,8 @@ class GameAPI:
116
115
  self,
117
116
  method: str,
118
117
  endpoint: str,
119
- params: Optional[Dict[str, Any]] = None,
120
- json_data: Optional[Dict[str, Any]] = None,
118
+ params: dict[str, Any] | None = None,
119
+ json_data: dict[str, Any] | None = None,
121
120
  ) -> Any:
122
121
  """Make an authenticated request to the API"""
123
122
  url = f"{self.base_url}/api/v1{endpoint}"
@@ -146,7 +145,7 @@ class GameAPI:
146
145
  try:
147
146
  error_detail = response.json()
148
147
  error_msg += f" - {error_detail.get('detail', 'Unknown error')}"
149
- except Exception:
148
+ except (AttributeError, TypeError, ValueError):
150
149
  error_msg += f" - {response.text}"
151
150
  raise GameAPIError(error_msg)
152
151
 
@@ -156,14 +155,14 @@ class GameAPI:
156
155
  raise GameAPIError(f"Network error: {e}") from e
157
156
 
158
157
  # Status endpoints
159
- async def get_status(self) -> Dict[str, Any]:
158
+ async def get_status(self) -> dict[str, Any]:
160
159
  """Get API status"""
161
160
  return await self._make_request("GET", "/status")
162
161
 
163
162
  # Trades endpoints
164
163
  async def get_trades(
165
- self, direction: Optional[str] = None, duration: Optional[str] = None
166
- ) -> List[Dict[str, Any]]:
164
+ self, direction: str | None = None, duration: str | None = None
165
+ ) -> list[dict[str, Any]]:
167
166
  """Get trades with optional filtering"""
168
167
  params = {}
169
168
  if direction:
@@ -173,14 +172,14 @@ class GameAPI:
173
172
  return await self._make_request("GET", "/trades", params=params)
174
173
 
175
174
  # Orders endpoints
176
- async def get_orders(self, direction: Optional[str] = None) -> List[Dict[str, Any]]:
175
+ async def get_orders(self, direction: str | None = None) -> list[dict[str, Any]]:
177
176
  """Get orders with optional filtering"""
178
177
  params = {}
179
178
  if direction:
180
179
  params["direction"] = direction
181
180
  return await self._make_request("GET", "/orders", params=params)
182
181
 
183
- async def find_order_by_id(self, order_id: str) -> Optional[Dict[str, Any]]:
182
+ async def find_order_by_id(self, order_id: str) -> dict[str, Any] | None:
184
183
  """
185
184
  Find a specific order by its ID (string or numeric)
186
185
 
@@ -201,11 +200,11 @@ class GameAPI:
201
200
  ):
202
201
  return order
203
202
  return None
204
- except Exception as e:
203
+ except (GameAPIError, AttributeError, TypeError, ValueError) as e:
205
204
  logger.error(f"Error finding order {order_id}: {e}")
206
205
  return None
207
206
 
208
- async def create_order(self, order_data: Dict[str, Any]) -> Dict[str, Any]:
207
+ async def create_order(self, order_data: dict[str, Any]) -> dict[str, Any]:
209
208
  """
210
209
  Create a new order
211
210
 
@@ -238,7 +237,7 @@ class GameAPI:
238
237
  self._validate_single_order(order_data, "Single order")
239
238
  return await self._make_request("POST", "/orders", json_data=order_data)
240
239
 
241
- def _validate_single_order(self, order: Dict[str, Any], context: str) -> None:
240
+ def _validate_single_order(self, order: dict[str, Any], context: str) -> None:
242
241
  """Validate a single order's required fields and apply defaults"""
243
242
  required_fields = ["side", "price", "volume", "date", "start_time", "end_time"]
244
243
  # Optional fields that are passed through to the API
@@ -313,7 +312,7 @@ class GameAPI:
313
312
  f"{context}: 'trader_suffixe' must be 50 characters or less"
314
313
  )
315
314
 
316
- async def delete_order(self, order_id: str) -> Dict[str, Any]:
315
+ async def delete_order(self, order_id: str) -> dict[str, Any]:
317
316
  """
318
317
  Delete an order by ID
319
318
 
@@ -344,7 +343,7 @@ class GameAPI:
344
343
  numeric_id = None
345
344
 
346
345
  # Method 1: Check for explicit numeric ID field
347
- if "id" in matching_order and matching_order["id"]:
346
+ if matching_order.get("id"):
348
347
  numeric_id = str(matching_order["id"])
349
348
  # Method 2: Check order_id_numeric field
350
349
  elif "order_id_numeric" in matching_order:
@@ -393,7 +392,7 @@ class GameAPI:
393
392
  return await self._make_request("DELETE", f"/orders/{numeric_id}")
394
393
 
395
394
  # Market endpoints
396
- def _sort_market_data(self, data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
395
+ def _sort_market_data(self, data: list[dict[str, Any]]) -> list[dict[str, Any]]:
397
396
  """
398
397
  Sort a market snapshot by contract/timestamp, then by direction,
399
398
  placing the "best" price first for each side.
@@ -402,7 +401,7 @@ class GameAPI:
402
401
  - buy/bid : higher price is better -> descending
403
402
  """
404
403
 
405
- def _sort_key(row: Dict[str, Any]):
404
+ def _sort_key(row: dict[str, Any]):
406
405
  contract = row.get("contract") or row.get("timestamp") or ""
407
406
  direction = row.get("direction") or ""
408
407
  price = row.get("price")
@@ -416,10 +415,10 @@ class GameAPI:
416
415
 
417
416
  async def get_market_data(
418
417
  self,
419
- direction: Optional[str] = None,
420
- duration: Optional[str] = None,
418
+ direction: str | None = None,
419
+ duration: str | None = None,
421
420
  sort_by_price: bool = False,
422
- ) -> List[Dict[str, Any]]:
421
+ ) -> list[dict[str, Any]]:
423
422
  """
424
423
  Get market data.
425
424
 
@@ -429,7 +428,7 @@ class GameAPI:
429
428
  sort_by_price: If True, sort the result so the best offer for each
430
429
  direction comes first (sell ascending, buy descending).
431
430
  """
432
- params: Dict[str, Any] = {}
431
+ params: dict[str, Any] = {}
433
432
  if direction:
434
433
  params["direction"] = direction
435
434
  if duration:
@@ -444,9 +443,9 @@ class GameAPI:
444
443
 
445
444
  async def get_market_best_offers(
446
445
  self,
447
- duration: Optional[str] = None,
448
- top_n: Optional[int] = None,
449
- ) -> Dict[str, Dict[str, List[Dict[str, Any]]]]:
446
+ duration: str | None = None,
447
+ top_n: int | None = None,
448
+ ) -> dict[str, dict[str, list[dict[str, Any]]]]:
450
449
  """
451
450
  Get market offers grouped by contract and direction, best prices first.
452
451
 
@@ -468,7 +467,7 @@ class GameAPI:
468
467
  }
469
468
  """
470
469
  data = await self.get_market_data(duration=duration, sort_by_price=True)
471
- grouped: Dict[str, Dict[str, List[Dict[str, Any]]]] = {}
470
+ grouped: dict[str, dict[str, list[dict[str, Any]]]] = {}
472
471
  for row in data:
473
472
  contract = row.get("contract") or row.get("timestamp") or ""
474
473
  direction = row.get("direction") or "unknown"
@@ -483,8 +482,8 @@ class GameAPI:
483
482
 
484
483
  # LTP (Last Traded Price) endpoints
485
484
  async def get_market_ltp(
486
- self, market_zone: Optional[str] = None
487
- ) -> List[Dict[str, Any]]:
485
+ self, market_zone: str | None = None
486
+ ) -> list[dict[str, Any]]:
488
487
  """
489
488
  Get last traded prices (LTP) for market contracts.
490
489
 
@@ -512,7 +511,23 @@ class GameAPI:
512
511
  params["market_zone"] = market_zone
513
512
  return await self._make_request("GET", "/market/market-ltp", params=params)
514
513
 
515
- async def get_latest_ltp(self) -> Dict[str, Dict[str, Any]]:
514
+ async def get_latest_available_ltp(
515
+ self, duration: str | None = None
516
+ ) -> list[dict[str, Any]]:
517
+ """Get the newest LTP compatible with available cross-border capacity.
518
+
519
+ The GAME API selects the newest eligible trade per contract, falling
520
+ back to an older trade when the newest foreign-zone LTP has no
521
+ bidirectional capacity with Switzerland. CH remains the fallback zone.
522
+ """
523
+ params: dict[str, Any] = {}
524
+ if duration:
525
+ params["duration"] = duration
526
+ return await self._make_request(
527
+ "GET", "/market/latest-ltp", params=params or None
528
+ )
529
+
530
+ async def get_latest_ltp(self) -> dict[str, dict[str, Any]]:
516
531
  """
517
532
  Get the latest LTP for each contract across all market zones.
518
533
 
@@ -545,7 +560,7 @@ class GameAPI:
545
560
  return {}
546
561
 
547
562
  # Group by contract and keep the most recent entry per contract
548
- latest_by_contract: Dict[str, Dict[str, Any]] = {}
563
+ latest_by_contract: dict[str, dict[str, Any]] = {}
549
564
 
550
565
  for entry in ltp_data:
551
566
  contract = entry.get("contract")
@@ -571,21 +586,39 @@ class GameAPI:
571
586
  latest_by_contract[contract]["market_zones"].add(zone)
572
587
 
573
588
  # Convert sets to lists for JSON serialization
574
- result: Dict[str, Dict[str, Any]] = {}
589
+ result: dict[str, dict[str, Any]] = {}
575
590
  for contract, data in latest_by_contract.items():
576
591
  result[contract] = {
577
592
  **data,
578
- "market_zones": sorted(list(data["market_zones"])),
593
+ "market_zones": sorted(data["market_zones"]),
579
594
  }
580
595
 
581
596
  return result
582
597
 
583
598
  # Spot endpoints
584
- async def get_spot_data(self) -> List[Dict[str, Any]]:
585
- """Get spot data"""
599
+ async def get_spot_data(
600
+ self,
601
+ area_name: str | None = None,
602
+ date_from: str | None = None,
603
+ date_to: str | None = None,
604
+ limit: int | None = None,
605
+ ) -> list[dict[str, Any]]:
606
+ """Get spot data with optional area, date range, and result limit filters."""
607
+ params = {
608
+ key: value
609
+ for key, value in {
610
+ "area_name": area_name,
611
+ "date_from": date_from,
612
+ "date_to": date_to,
613
+ "limit": limit,
614
+ }.items()
615
+ if value is not None
616
+ }
617
+ if params:
618
+ return await self._make_request("GET", "/spot", params=params)
586
619
  return await self._make_request("GET", "/spot")
587
620
 
588
- async def get_spot_status(self) -> Dict[str, Any]:
621
+ async def get_spot_status(self) -> dict[str, Any]:
589
622
  """Get spot API status"""
590
623
  return await self._make_request("GET", "/spot/status")
591
624
 
@@ -595,6 +628,6 @@ class GameAPI:
595
628
  try:
596
629
  await self.get_status()
597
630
  return True
598
- except Exception as e:
631
+ except GameAPIError as e:
599
632
  logger.error(f"Connection test failed: {e}")
600
633
  return False
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "game-api-client"
3
- version = "0.1.0"
3
+ version = "0.1.2"
4
4
  description = "Python client library for GAME trading API"
5
5
  authors = ["Groupe E <support@groupe-e.ch>"]
6
6
  readme = "README.md"
@@ -51,18 +51,18 @@ build-backend = "poetry.core.masonry.api"
51
51
 
52
52
  [tool.black]
53
53
  line-length = 88
54
- target-version = "0.1.0"
54
+ target-version = "0.1.2"
55
55
 
56
56
  [tool.ruff]
57
57
  line-length = 88
58
- target-version = "0.1.0"
58
+ target-version = "0.1.2"
59
59
 
60
60
  [tool.ruff.lint]
61
61
  select = ["E", "F", "W", "I", "N", "B", "C90"]
62
62
  ignore = ["E501", "B008", "N999", "C901"]
63
63
 
64
64
  [tool.mypy]
65
- python_version = "0.1.0"
65
+ python_version = "0.1.2"
66
66
  warn_return_any = true
67
67
  warn_unused_configs = true
68
68
  disallow_untyped_defs = true