fivetwenty 0.1.0__py3-none-any.whl

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.
fivetwenty/__init__.py ADDED
@@ -0,0 +1,62 @@
1
+ """
2
+ OANDA REST API v20 Python SDK
3
+
4
+ A simple, elegant Python client for OANDA's REST API v20.
5
+
6
+ Usage:
7
+ from fivetwenty import Client, AsyncClient, Environment, AccountConfig
8
+
9
+ # Method 1: Direct parameters
10
+ async with AsyncClient(token="your-token", environment=Environment.PRACTICE) as client:
11
+ accounts = await client.accounts.list()
12
+
13
+ # Method 2: Configuration object
14
+ config = AccountConfig(
15
+ token="your-token",
16
+ account_id="your-account-id",
17
+ environment=Environment.PRACTICE,
18
+ alias="my_account"
19
+ )
20
+ async with AsyncClient(config=config) as client:
21
+ accounts = await client.accounts.list()
22
+
23
+ # Method 3: Environment variables (fallback)
24
+ # Set FIVETWENTY_OANDA_TOKEN, FIVETWENTY_OANDA_ACCOUNT, etc.
25
+ async with AsyncClient() as client:
26
+ accounts = await client.accounts.list()
27
+
28
+ # Sync wrapper (same patterns)
29
+ with Client(token="your-token") as client:
30
+ accounts = client.accounts.list()
31
+ """
32
+
33
+ __version__ = "20.1.0"
34
+
35
+ from ._internal.environment import Environment
36
+ from .client import AsyncClient, Client
37
+ from .configuration import AccountConfig, AccountConfigLoader, ConfigValidator
38
+ from .exceptions import FiveTwentyError, StreamStall
39
+ from .models import ErrorCategory, ErrorDetails, ErrorSeverity, FiveTwentyErrorCode, ValidationViolation
40
+
41
+ __all__ = [
42
+ # Configuration
43
+ "AccountConfig",
44
+ "AccountConfigLoader",
45
+ # Main clients
46
+ "AsyncClient",
47
+ "Client",
48
+ "ConfigValidator",
49
+ # Enums
50
+ "Environment",
51
+ # Error handling
52
+ "ErrorCategory",
53
+ "ErrorDetails",
54
+ "ErrorSeverity",
55
+ # Exceptions
56
+ "FiveTwentyError",
57
+ "FiveTwentyErrorCode",
58
+ "StreamStall",
59
+ "ValidationViolation",
60
+ # Version
61
+ "__version__",
62
+ ]
@@ -0,0 +1 @@
1
+ """Internal utilities and helpers."""
@@ -0,0 +1,17 @@
1
+ """Environment configuration."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class Environment(Enum):
7
+ """OANDA API environments."""
8
+
9
+ PRACTICE = "practice"
10
+ LIVE = "live"
11
+
12
+ @property
13
+ def base_url(self) -> str:
14
+ """Get the base URL for this environment."""
15
+ if self == Environment.LIVE:
16
+ return "https://api-fxtrade.oanda.com/v3"
17
+ return "https://api-fxpractice.oanda.com/v3"
@@ -0,0 +1,113 @@
1
+ """Internal utility functions."""
2
+
3
+ import random
4
+ import sys
5
+ from decimal import Decimal
6
+ from time import monotonic
7
+ from typing import Any
8
+
9
+ try:
10
+ import httpx
11
+
12
+ from .. import __version__
13
+ except ImportError:
14
+ # Handle cases where these aren't available during early setup
15
+ httpx = None # type: ignore
16
+ __version__ = "20.1.0"
17
+
18
+
19
+ def backoff_with_jitter(attempt: int, base: float = 0.5, cap: float = 8.0) -> float:
20
+ """
21
+ Calculate exponential backoff with jitter.
22
+
23
+ Args:
24
+ attempt: The attempt number (0-indexed)
25
+ base: Base delay in seconds
26
+ cap: Maximum delay in seconds
27
+
28
+ Returns:
29
+ Delay in seconds with jitter applied
30
+ """
31
+ delay = min(cap, base * (2**attempt))
32
+ # Add jitter: 50% to 100% of calculated delay
33
+ jitter = random.random() / 2.0 # 0 to 0.5
34
+ result: float = delay * (0.5 + jitter)
35
+ return result
36
+
37
+
38
+ def build_user_agent() -> str:
39
+ """Build User-Agent string with version info."""
40
+ import os
41
+
42
+ # Base user agent
43
+ base = f"fivetwenty/{__version__} (python-{sys.version_info[0]}.{sys.version_info[1]}"
44
+ if httpx:
45
+ base += f"; httpx-{httpx.__version__}"
46
+ base += ")"
47
+
48
+ # Optional extra from environment
49
+ extra = os.environ.get("FIVETWENTY_USER_AGENT_EXTRA")
50
+ return f"{base} {extra}" if extra else base
51
+
52
+
53
+ def stringify_decimals(obj: Any) -> Any:
54
+ """
55
+ Recursively convert all Decimals to strings in a data structure.
56
+
57
+ This prevents future misses when new Decimal fields appear in the API.
58
+
59
+ Args:
60
+ obj: The object to process
61
+
62
+ Returns:
63
+ The object with all Decimals converted to strings
64
+ """
65
+ if isinstance(obj, Decimal):
66
+ return format(obj, "f")
67
+ if isinstance(obj, list):
68
+ return [stringify_decimals(item) for item in obj]
69
+ if isinstance(obj, dict):
70
+ return {key: stringify_decimals(value) for key, value in obj.items()}
71
+ return obj
72
+
73
+
74
+ def quantize_price(precision: int, value: Decimal) -> Decimal:
75
+ """
76
+ Round a price to the specified precision.
77
+
78
+ Args:
79
+ precision: Number of decimal places
80
+ value: The price to quantize
81
+
82
+ Returns:
83
+ The quantized price
84
+ """
85
+ quantizer = Decimal(10) ** (-precision)
86
+ return value.quantize(quantizer)
87
+
88
+
89
+ class MonotonicTimeout:
90
+ """Helper for timeout tracking using monotonic time."""
91
+
92
+ def __init__(self, timeout_seconds: float):
93
+ self.timeout_seconds = timeout_seconds
94
+ self.start_time = monotonic()
95
+
96
+ @property
97
+ def elapsed(self) -> float:
98
+ """Get elapsed time in seconds."""
99
+ return monotonic() - self.start_time
100
+
101
+ @property
102
+ def remaining(self) -> float:
103
+ """Get remaining time in seconds (may be negative if expired)."""
104
+ return self.timeout_seconds - self.elapsed
105
+
106
+ @property
107
+ def expired(self) -> bool:
108
+ """Check if timeout has expired."""
109
+ return self.elapsed >= self.timeout_seconds
110
+
111
+ def sleep_remaining(self, max_sleep: float = 1.0) -> float:
112
+ """Get sleep time, capped at max_sleep."""
113
+ return min(max_sleep, max(0, self.remaining))