europatech 1.0.0__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.
- europatech-1.0.0/LICENSE +21 -0
- europatech-1.0.0/PKG-INFO +133 -0
- europatech-1.0.0/README.md +109 -0
- europatech-1.0.0/europatech/__init__.py +34 -0
- europatech-1.0.0/europatech/client.py +201 -0
- europatech-1.0.0/europatech/py.typed +0 -0
- europatech-1.0.0/europatech/types.py +127 -0
- europatech-1.0.0/europatech/webhooks/__init__.py +77 -0
- europatech-1.0.0/pyproject.toml +30 -0
europatech-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Europa Tech Enterprise S.R.L.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: europatech
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for the Europa Tech real estate tokenization API
|
|
5
|
+
Project-URL: Homepage, https://europa-tech.org/docs/api
|
|
6
|
+
Project-URL: Repository, https://github.com/europa-tech-srl/europatech-sdk-python
|
|
7
|
+
Project-URL: Documentation, https://europa-tech.org/docs/api
|
|
8
|
+
Author-email: "Europa Tech Enterprise S.R.L." <admin@europa-tech.org>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,europa-tech,europatech,mica,real-estate,rwa,sdk,tokenization
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# europatech
|
|
26
|
+
|
|
27
|
+
Official Python SDK for the [Europa Tech](https://europa-tech.org) real estate tokenization API.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install europatech
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick Start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from europatech import EuropaTech
|
|
39
|
+
|
|
40
|
+
client = EuropaTech(api_key="your-api-key")
|
|
41
|
+
|
|
42
|
+
# List available properties
|
|
43
|
+
objects = client.list_objects()
|
|
44
|
+
for obj in objects:
|
|
45
|
+
print(f"{obj.name}: {obj.available_shares} shares available")
|
|
46
|
+
|
|
47
|
+
# Get your portfolio
|
|
48
|
+
portfolio = client.get_portfolio()
|
|
49
|
+
print(f"Total value: EUR {portfolio.total_value}")
|
|
50
|
+
|
|
51
|
+
# Get room prices
|
|
52
|
+
rooms = client.list_rooms("obj-001")
|
|
53
|
+
for room in rooms:
|
|
54
|
+
print(f"{room.name}: EUR {room.share_price}/share")
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Reference
|
|
58
|
+
|
|
59
|
+
### Objects
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
client.list_objects() # List all properties
|
|
63
|
+
client.get_object("obj-001") # Get property details
|
|
64
|
+
client.list_rooms("obj-001") # Get rooms with share prices
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Portfolio
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
client.get_portfolio() # Your holdings
|
|
71
|
+
client.list_transactions(page=1, limit=20) # Transaction history
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### P2P Marketplace
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
client.list_p2p() # Active listings
|
|
78
|
+
client.create_p2p_listing( # Sell shares
|
|
79
|
+
object_id="obj-001",
|
|
80
|
+
room_id="room-101",
|
|
81
|
+
shares=5,
|
|
82
|
+
price_per_share=70.0,
|
|
83
|
+
)
|
|
84
|
+
client.cancel_p2p_listing("listing-id") # Cancel listing
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Investment
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
client.invest( # Buy shares
|
|
91
|
+
object_id="obj-001",
|
|
92
|
+
room_id="room-101",
|
|
93
|
+
shares=10,
|
|
94
|
+
payment_method="CARD",
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Market & Fund
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
client.get_market_rates() # EUR/USD, BTC, ETH rates
|
|
102
|
+
client.get_fund() # AUM, distributions
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Compliance
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
client.get_compliance_status() # KYC/AML status
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Webhooks
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from europatech.webhooks import verify_webhook, parse_webhook_event
|
|
115
|
+
|
|
116
|
+
# Verify signature
|
|
117
|
+
is_valid = verify_webhook(request.body, request.headers["X-EuropaTech-Signature"], secret)
|
|
118
|
+
|
|
119
|
+
# Parse event
|
|
120
|
+
event = parse_webhook_event(request.body)
|
|
121
|
+
print(event.type) # "investment.completed"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Context Manager
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
with EuropaTech(api_key="your-key") as client:
|
|
128
|
+
objects = client.list_objects()
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT — Europa Tech Enterprise S.R.L.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# europatech
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [Europa Tech](https://europa-tech.org) real estate tokenization API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install europatech
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from europatech import EuropaTech
|
|
15
|
+
|
|
16
|
+
client = EuropaTech(api_key="your-api-key")
|
|
17
|
+
|
|
18
|
+
# List available properties
|
|
19
|
+
objects = client.list_objects()
|
|
20
|
+
for obj in objects:
|
|
21
|
+
print(f"{obj.name}: {obj.available_shares} shares available")
|
|
22
|
+
|
|
23
|
+
# Get your portfolio
|
|
24
|
+
portfolio = client.get_portfolio()
|
|
25
|
+
print(f"Total value: EUR {portfolio.total_value}")
|
|
26
|
+
|
|
27
|
+
# Get room prices
|
|
28
|
+
rooms = client.list_rooms("obj-001")
|
|
29
|
+
for room in rooms:
|
|
30
|
+
print(f"{room.name}: EUR {room.share_price}/share")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API Reference
|
|
34
|
+
|
|
35
|
+
### Objects
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
client.list_objects() # List all properties
|
|
39
|
+
client.get_object("obj-001") # Get property details
|
|
40
|
+
client.list_rooms("obj-001") # Get rooms with share prices
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Portfolio
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
client.get_portfolio() # Your holdings
|
|
47
|
+
client.list_transactions(page=1, limit=20) # Transaction history
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### P2P Marketplace
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
client.list_p2p() # Active listings
|
|
54
|
+
client.create_p2p_listing( # Sell shares
|
|
55
|
+
object_id="obj-001",
|
|
56
|
+
room_id="room-101",
|
|
57
|
+
shares=5,
|
|
58
|
+
price_per_share=70.0,
|
|
59
|
+
)
|
|
60
|
+
client.cancel_p2p_listing("listing-id") # Cancel listing
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Investment
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
client.invest( # Buy shares
|
|
67
|
+
object_id="obj-001",
|
|
68
|
+
room_id="room-101",
|
|
69
|
+
shares=10,
|
|
70
|
+
payment_method="CARD",
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Market & Fund
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
client.get_market_rates() # EUR/USD, BTC, ETH rates
|
|
78
|
+
client.get_fund() # AUM, distributions
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Compliance
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
client.get_compliance_status() # KYC/AML status
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Webhooks
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from europatech.webhooks import verify_webhook, parse_webhook_event
|
|
91
|
+
|
|
92
|
+
# Verify signature
|
|
93
|
+
is_valid = verify_webhook(request.body, request.headers["X-EuropaTech-Signature"], secret)
|
|
94
|
+
|
|
95
|
+
# Parse event
|
|
96
|
+
event = parse_webhook_event(request.body)
|
|
97
|
+
print(event.type) # "investment.completed"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Context Manager
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
with EuropaTech(api_key="your-key") as client:
|
|
104
|
+
objects = client.list_objects()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT — Europa Tech Enterprise S.R.L.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Official Python SDK for the Europa Tech real estate tokenization API."""
|
|
2
|
+
|
|
3
|
+
from europatech.client import EuropaTech
|
|
4
|
+
from europatech.types import (
|
|
5
|
+
RealEstateObject,
|
|
6
|
+
Room,
|
|
7
|
+
Portfolio,
|
|
8
|
+
Holding,
|
|
9
|
+
Transaction,
|
|
10
|
+
P2PListing,
|
|
11
|
+
MarketRates,
|
|
12
|
+
InvestResult,
|
|
13
|
+
Fund,
|
|
14
|
+
Distribution,
|
|
15
|
+
ComplianceStatus,
|
|
16
|
+
ApiError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__version__ = "1.0.0"
|
|
20
|
+
__all__ = [
|
|
21
|
+
"EuropaTech",
|
|
22
|
+
"RealEstateObject",
|
|
23
|
+
"Room",
|
|
24
|
+
"Portfolio",
|
|
25
|
+
"Holding",
|
|
26
|
+
"Transaction",
|
|
27
|
+
"P2PListing",
|
|
28
|
+
"MarketRates",
|
|
29
|
+
"InvestResult",
|
|
30
|
+
"Fund",
|
|
31
|
+
"Distribution",
|
|
32
|
+
"ComplianceStatus",
|
|
33
|
+
"ApiError",
|
|
34
|
+
]
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Europa Tech API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from europatech.types import (
|
|
10
|
+
ApiError,
|
|
11
|
+
ComplianceStatus,
|
|
12
|
+
Distribution,
|
|
13
|
+
Fund,
|
|
14
|
+
Holding,
|
|
15
|
+
InvestResult,
|
|
16
|
+
MarketRates,
|
|
17
|
+
P2PListing,
|
|
18
|
+
Portfolio,
|
|
19
|
+
RealEstateObject,
|
|
20
|
+
Room,
|
|
21
|
+
Transaction,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
DEFAULT_BASE_URL = "https://api.europa-tech.org"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _to_snake(name: str) -> str:
|
|
28
|
+
"""Convert camelCase to snake_case."""
|
|
29
|
+
result: list[str] = []
|
|
30
|
+
for c in name:
|
|
31
|
+
if c.isupper() and result:
|
|
32
|
+
result.append("_")
|
|
33
|
+
result.append(c.lower())
|
|
34
|
+
return "".join(result)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _snake_keys(data: Any) -> Any:
|
|
38
|
+
"""Recursively convert dict keys from camelCase to snake_case."""
|
|
39
|
+
if isinstance(data, dict):
|
|
40
|
+
return {_to_snake(k): _snake_keys(v) for k, v in data.items()}
|
|
41
|
+
if isinstance(data, list):
|
|
42
|
+
return [_snake_keys(item) for item in data]
|
|
43
|
+
return data
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class EuropaTech:
|
|
47
|
+
"""Official Europa Tech API client.
|
|
48
|
+
|
|
49
|
+
Usage::
|
|
50
|
+
|
|
51
|
+
from europatech import EuropaTech
|
|
52
|
+
|
|
53
|
+
client = EuropaTech(api_key="your-api-key")
|
|
54
|
+
objects = client.list_objects()
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
api_key: str,
|
|
60
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
61
|
+
timeout: float = 30.0,
|
|
62
|
+
) -> None:
|
|
63
|
+
if not api_key:
|
|
64
|
+
raise ValueError("api_key is required")
|
|
65
|
+
self._client = httpx.Client(
|
|
66
|
+
base_url=base_url.rstrip("/"),
|
|
67
|
+
headers={
|
|
68
|
+
"Authorization": f"Bearer {api_key}",
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"X-SDK-Version": "python/1.0.0",
|
|
71
|
+
},
|
|
72
|
+
timeout=timeout,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def close(self) -> None:
|
|
76
|
+
"""Close the HTTP client."""
|
|
77
|
+
self._client.close()
|
|
78
|
+
|
|
79
|
+
def __enter__(self) -> "EuropaTech":
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def __exit__(self, *_: Any) -> None:
|
|
83
|
+
self.close()
|
|
84
|
+
|
|
85
|
+
def _request(self, method: str, path: str, json: Any = None) -> Any:
|
|
86
|
+
resp = self._client.request(method, path, json=json)
|
|
87
|
+
if resp.status_code >= 400:
|
|
88
|
+
try:
|
|
89
|
+
body = resp.json()
|
|
90
|
+
msg = body.get("error", resp.reason_phrase)
|
|
91
|
+
code = body.get("code", "UNKNOWN_ERROR")
|
|
92
|
+
except Exception:
|
|
93
|
+
msg = resp.reason_phrase
|
|
94
|
+
code = "UNKNOWN_ERROR"
|
|
95
|
+
raise ApiError(resp.status_code, msg, code)
|
|
96
|
+
return resp.json()
|
|
97
|
+
|
|
98
|
+
# ─── Objects ──────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
def list_objects(self) -> list[RealEstateObject]:
|
|
101
|
+
"""List all real estate objects."""
|
|
102
|
+
data = self._request("GET", "/api/v1/objects")["data"]
|
|
103
|
+
return [RealEstateObject(**_snake_keys(o)) for o in data]
|
|
104
|
+
|
|
105
|
+
def get_object(self, object_id: str) -> RealEstateObject:
|
|
106
|
+
"""Get a single object by ID."""
|
|
107
|
+
data = self._request("GET", f"/api/v1/objects/{object_id}")["data"]
|
|
108
|
+
return RealEstateObject(**_snake_keys(data))
|
|
109
|
+
|
|
110
|
+
def list_rooms(self, object_id: str) -> list[Room]:
|
|
111
|
+
"""Get rooms for an object."""
|
|
112
|
+
data = self._request("GET", f"/api/v1/objects/{object_id}/rooms")["data"]
|
|
113
|
+
return [Room(**_snake_keys(r)) for r in data]
|
|
114
|
+
|
|
115
|
+
# ─── Portfolio ────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
def get_portfolio(self) -> Portfolio:
|
|
118
|
+
"""Get current user's portfolio."""
|
|
119
|
+
raw = _snake_keys(self._request("GET", "/api/v1/portfolio")["data"])
|
|
120
|
+
raw["holdings"] = [Holding(**h) for h in raw.get("holdings", [])]
|
|
121
|
+
return Portfolio(**raw)
|
|
122
|
+
|
|
123
|
+
def list_transactions(
|
|
124
|
+
self,
|
|
125
|
+
page: int = 1,
|
|
126
|
+
limit: int = 20,
|
|
127
|
+
type: Optional[str] = None,
|
|
128
|
+
) -> list[Transaction]:
|
|
129
|
+
"""List portfolio transactions."""
|
|
130
|
+
params = f"?page={page}&limit={limit}"
|
|
131
|
+
if type:
|
|
132
|
+
params += f"&type={type}"
|
|
133
|
+
data = self._request("GET", f"/api/v1/portfolio/transactions{params}")["data"]
|
|
134
|
+
return [Transaction(**_snake_keys(t)) for t in data]
|
|
135
|
+
|
|
136
|
+
# ─── P2P Marketplace ──────────────────────────────────
|
|
137
|
+
|
|
138
|
+
def list_p2p(self) -> list[P2PListing]:
|
|
139
|
+
"""List P2P listings."""
|
|
140
|
+
data = self._request("GET", "/api/v1/p2p")["data"]
|
|
141
|
+
return [P2PListing(**_snake_keys(l)) for l in data]
|
|
142
|
+
|
|
143
|
+
def create_p2p_listing(
|
|
144
|
+
self,
|
|
145
|
+
object_id: str,
|
|
146
|
+
room_id: str,
|
|
147
|
+
shares: int,
|
|
148
|
+
price_per_share: float,
|
|
149
|
+
) -> P2PListing:
|
|
150
|
+
"""Create a P2P sell listing."""
|
|
151
|
+
data = self._request("POST", "/api/v1/p2p", json={
|
|
152
|
+
"objectId": object_id,
|
|
153
|
+
"roomId": room_id,
|
|
154
|
+
"shares": shares,
|
|
155
|
+
"pricePerShare": price_per_share,
|
|
156
|
+
})["data"]
|
|
157
|
+
return P2PListing(**_snake_keys(data))
|
|
158
|
+
|
|
159
|
+
def cancel_p2p_listing(self, listing_id: str) -> None:
|
|
160
|
+
"""Cancel a P2P listing."""
|
|
161
|
+
self._request("DELETE", f"/api/v1/p2p/{listing_id}")
|
|
162
|
+
|
|
163
|
+
# ─── Market ───────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
def get_market_rates(self) -> MarketRates:
|
|
166
|
+
"""Get current market rates."""
|
|
167
|
+
data = self._request("GET", "/api/v1/market/rates")["data"]
|
|
168
|
+
return MarketRates(**_snake_keys(data))
|
|
169
|
+
|
|
170
|
+
# ─── Investment ───────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
def invest(
|
|
173
|
+
self,
|
|
174
|
+
object_id: str,
|
|
175
|
+
room_id: str,
|
|
176
|
+
shares: int,
|
|
177
|
+
payment_method: str = "CARD",
|
|
178
|
+
) -> InvestResult:
|
|
179
|
+
"""Execute a share purchase."""
|
|
180
|
+
data = self._request("POST", "/api/v1/invest", json={
|
|
181
|
+
"objectId": object_id,
|
|
182
|
+
"roomId": room_id,
|
|
183
|
+
"shares": shares,
|
|
184
|
+
"paymentMethod": payment_method,
|
|
185
|
+
})["data"]
|
|
186
|
+
return InvestResult(**_snake_keys(data))
|
|
187
|
+
|
|
188
|
+
# ─── Fund ─────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
def get_fund(self) -> Fund:
|
|
191
|
+
"""Get fund overview."""
|
|
192
|
+
raw = _snake_keys(self._request("GET", "/api/v1/fund")["data"])
|
|
193
|
+
raw["distributions"] = [Distribution(**d) for d in raw.get("distributions", [])]
|
|
194
|
+
return Fund(**raw)
|
|
195
|
+
|
|
196
|
+
# ─── Compliance ───────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
def get_compliance_status(self) -> ComplianceStatus:
|
|
199
|
+
"""Get current user's KYC/AML compliance status."""
|
|
200
|
+
data = self._request("GET", "/api/v1/compliance/status")["data"]
|
|
201
|
+
return ComplianceStatus(**_snake_keys(data))
|
|
File without changes
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Type definitions for Europa Tech API responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ApiError(Exception):
|
|
10
|
+
"""API error with status code and error code."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, status: int, message: str, code: str = "UNKNOWN_ERROR") -> None:
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
self.status = status
|
|
15
|
+
self.code = code
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class Room:
|
|
20
|
+
id: str
|
|
21
|
+
name: str
|
|
22
|
+
number: int
|
|
23
|
+
share_price: float
|
|
24
|
+
total_shares: int
|
|
25
|
+
available_shares: int
|
|
26
|
+
status: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class RealEstateObject:
|
|
31
|
+
id: str
|
|
32
|
+
name: str
|
|
33
|
+
location: str
|
|
34
|
+
image_url: str
|
|
35
|
+
status: str
|
|
36
|
+
total_shares: int
|
|
37
|
+
available_shares: int
|
|
38
|
+
rooms: list[Room] = field(default_factory=list)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class Holding:
|
|
43
|
+
object_id: str
|
|
44
|
+
object_name: str
|
|
45
|
+
room_id: str
|
|
46
|
+
room_name: str
|
|
47
|
+
shares: int
|
|
48
|
+
share_price: float
|
|
49
|
+
value: float
|
|
50
|
+
purchased_at: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class Portfolio:
|
|
55
|
+
total_value: float
|
|
56
|
+
total_shares: int
|
|
57
|
+
total_invested: float
|
|
58
|
+
holdings: list[Holding] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class Transaction:
|
|
63
|
+
id: str
|
|
64
|
+
type: str
|
|
65
|
+
object_id: str
|
|
66
|
+
room_id: str
|
|
67
|
+
shares: int
|
|
68
|
+
price_per_share: float
|
|
69
|
+
total_amount: float
|
|
70
|
+
status: str
|
|
71
|
+
created_at: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class P2PListing:
|
|
76
|
+
id: str
|
|
77
|
+
object_id: str
|
|
78
|
+
room_id: str
|
|
79
|
+
seller_id: str
|
|
80
|
+
shares: int
|
|
81
|
+
price_per_share: float
|
|
82
|
+
status: str
|
|
83
|
+
created_at: str
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class MarketRates:
|
|
88
|
+
eur_usd: float
|
|
89
|
+
btc_eur: float
|
|
90
|
+
eth_eur: float
|
|
91
|
+
updated_at: str
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class InvestResult:
|
|
96
|
+
transaction_id: str
|
|
97
|
+
status: str
|
|
98
|
+
total_amount: float
|
|
99
|
+
currency: str
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class Distribution:
|
|
104
|
+
month: str
|
|
105
|
+
revenue: float
|
|
106
|
+
expenses: float
|
|
107
|
+
net_profit: float
|
|
108
|
+
distributed_per_share: float
|
|
109
|
+
status: str
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class Fund:
|
|
114
|
+
total_aum: float
|
|
115
|
+
total_investors: int
|
|
116
|
+
total_properties: int
|
|
117
|
+
total_rooms: int
|
|
118
|
+
distributions: list[Distribution] = field(default_factory=list)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(frozen=True)
|
|
122
|
+
class ComplianceStatus:
|
|
123
|
+
kyc_status: str
|
|
124
|
+
kyc_level: int
|
|
125
|
+
aml_status: str
|
|
126
|
+
investment_limit: float
|
|
127
|
+
invested_amount: float
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Webhook verification for Europa Tech."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Literal
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def verify_webhook(payload: str | bytes, signature: str, secret: str) -> bool:
|
|
13
|
+
"""Verify an incoming Europa Tech webhook signature (HMAC-SHA256).
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
payload: Raw request body.
|
|
17
|
+
signature: Value of the X-EuropaTech-Signature header.
|
|
18
|
+
secret: Your webhook secret from the dashboard.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
True if the signature is valid.
|
|
22
|
+
|
|
23
|
+
Example::
|
|
24
|
+
|
|
25
|
+
from europatech.webhooks import verify_webhook
|
|
26
|
+
|
|
27
|
+
@app.post("/webhook")
|
|
28
|
+
def handle_webhook(request):
|
|
29
|
+
sig = request.headers["X-EuropaTech-Signature"]
|
|
30
|
+
if not verify_webhook(request.body, sig, WEBHOOK_SECRET):
|
|
31
|
+
return {"error": "Invalid signature"}, 401
|
|
32
|
+
event = parse_webhook_event(request.body)
|
|
33
|
+
...
|
|
34
|
+
"""
|
|
35
|
+
if not payload or not signature or not secret:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
if isinstance(payload, str):
|
|
39
|
+
payload = payload.encode()
|
|
40
|
+
|
|
41
|
+
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
|
|
42
|
+
sig = signature.removeprefix("sha256=")
|
|
43
|
+
|
|
44
|
+
return hmac.compare_digest(expected, sig)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
WebhookEventType = Literal[
|
|
48
|
+
"investment.completed",
|
|
49
|
+
"investment.failed",
|
|
50
|
+
"p2p.listing.created",
|
|
51
|
+
"p2p.listing.sold",
|
|
52
|
+
"p2p.listing.cancelled",
|
|
53
|
+
"distribution.published",
|
|
54
|
+
"kyc.approved",
|
|
55
|
+
"kyc.rejected",
|
|
56
|
+
"transfer.completed",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class WebhookEvent:
|
|
62
|
+
id: str
|
|
63
|
+
type: WebhookEventType
|
|
64
|
+
created_at: str
|
|
65
|
+
data: dict[str, Any]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_webhook_event(payload: str | bytes) -> WebhookEvent:
|
|
69
|
+
"""Parse a webhook payload into a typed event object."""
|
|
70
|
+
raw = payload if isinstance(payload, str) else payload.decode()
|
|
71
|
+
d = json.loads(raw)
|
|
72
|
+
return WebhookEvent(
|
|
73
|
+
id=d["id"],
|
|
74
|
+
type=d["type"],
|
|
75
|
+
created_at=d.get("createdAt", ""),
|
|
76
|
+
data=d.get("data", {}),
|
|
77
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "europatech"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python SDK for the Europa Tech real estate tokenization API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Europa Tech Enterprise S.R.L.", email = "admin@europa-tech.org" }]
|
|
13
|
+
keywords = ["europatech", "europa-tech", "real-estate", "tokenization", "rwa", "api", "sdk", "mica"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = ["httpx>=0.27"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://europa-tech.org/docs/api"
|
|
29
|
+
Repository = "https://github.com/europa-tech-srl/europatech-sdk-python"
|
|
30
|
+
Documentation = "https://europa-tech.org/docs/api"
|