mvr-api-client 2.6.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.
- mvr_api/__init__.py +58 -0
- mvr_api/client.py +344 -0
- mvr_api/models.py +251 -0
- mvr_api_client-2.6.0.dist-info/METADATA +187 -0
- mvr_api_client-2.6.0.dist-info/RECORD +8 -0
- mvr_api_client-2.6.0.dist-info/WHEEL +5 -0
- mvr_api_client-2.6.0.dist-info/licenses/LICENSE +21 -0
- mvr_api_client-2.6.0.dist-info/top_level.txt +1 -0
mvr_api/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""African Market OS MVR API Client"""
|
|
2
|
+
|
|
3
|
+
from .client import MVRApiClient, SessionMVRApiClient
|
|
4
|
+
from .models import (
|
|
5
|
+
MVRApiConfig,
|
|
6
|
+
MVRScoreResponse,
|
|
7
|
+
SurveyAggregateRequest,
|
|
8
|
+
SurveyAggregateResponse,
|
|
9
|
+
TrendsResponse,
|
|
10
|
+
ForecastRequest,
|
|
11
|
+
ForecastResponse,
|
|
12
|
+
CompareRequest,
|
|
13
|
+
CompareResponse,
|
|
14
|
+
BenchmarkResponse,
|
|
15
|
+
InsightsResponse,
|
|
16
|
+
TemperatureResponse,
|
|
17
|
+
PolicyAuditResponse,
|
|
18
|
+
StoryResponse,
|
|
19
|
+
MetaResponse,
|
|
20
|
+
UsageResponse,
|
|
21
|
+
WhoAmIResponse,
|
|
22
|
+
DocsResponse,
|
|
23
|
+
SessionResponse,
|
|
24
|
+
HealthResponse,
|
|
25
|
+
MVRError,
|
|
26
|
+
AttributionObject,
|
|
27
|
+
VersionInfo,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__version__ = "2.6.0"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"MVRApiClient",
|
|
34
|
+
"SessionMVRApiClient",
|
|
35
|
+
"MVRApiConfig",
|
|
36
|
+
"MVRScoreResponse",
|
|
37
|
+
"SurveyAggregateRequest",
|
|
38
|
+
"SurveyAggregateResponse",
|
|
39
|
+
"TrendsResponse",
|
|
40
|
+
"ForecastRequest",
|
|
41
|
+
"ForecastResponse",
|
|
42
|
+
"CompareRequest",
|
|
43
|
+
"CompareResponse",
|
|
44
|
+
"BenchmarkResponse",
|
|
45
|
+
"InsightsResponse",
|
|
46
|
+
"TemperatureResponse",
|
|
47
|
+
"PolicyAuditResponse",
|
|
48
|
+
"StoryResponse",
|
|
49
|
+
"MetaResponse",
|
|
50
|
+
"UsageResponse",
|
|
51
|
+
"WhoAmIResponse",
|
|
52
|
+
"DocsResponse",
|
|
53
|
+
"SessionResponse",
|
|
54
|
+
"HealthResponse",
|
|
55
|
+
"MVRError",
|
|
56
|
+
"AttributionObject",
|
|
57
|
+
"VersionInfo",
|
|
58
|
+
]
|
mvr_api/client.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import requests
|
|
3
|
+
from typing import Optional, Dict, Any
|
|
4
|
+
from .models import (
|
|
5
|
+
MVRApiConfig, MVRScoreResponse, SurveyAggregateRequest,
|
|
6
|
+
SurveyAggregateResponse, TrendsResponse, ForecastRequest,
|
|
7
|
+
ForecastResponse, CompareRequest, CompareResponse,
|
|
8
|
+
BenchmarkResponse, InsightsResponse, TemperatureResponse,
|
|
9
|
+
PolicyAuditResponse, StoryResponse, MetaResponse,
|
|
10
|
+
UsageResponse, WhoAmIResponse, DocsResponse, SessionResponse,
|
|
11
|
+
HealthResponse, MVRError, AttributionObject
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MVRApiError(Exception):
|
|
16
|
+
"""Base exception for MVR API errors."""
|
|
17
|
+
def __init__(self, error_data: MVRError):
|
|
18
|
+
self.error_data = error_data
|
|
19
|
+
super().__init__(f"{error_data.error_code}: {error_data.message}")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MVRApiClient:
|
|
23
|
+
"""Main MVR API client using License + Email authentication."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, config: MVRApiConfig):
|
|
26
|
+
self.config = config
|
|
27
|
+
|
|
28
|
+
# Persistent session
|
|
29
|
+
self.session = requests.Session()
|
|
30
|
+
self.session.headers.update({
|
|
31
|
+
"x-mvr-license": config.license,
|
|
32
|
+
"x-buyer-email": config.email,
|
|
33
|
+
"Content-Type": "application/json",
|
|
34
|
+
"User-Agent": "mvr-api-py-client/2.6.0"
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
self.base_url = config.base_url
|
|
38
|
+
self.max_retries = config.max_retries
|
|
39
|
+
self.timeout = config.timeout
|
|
40
|
+
|
|
41
|
+
# ------------------------------------------------------------
|
|
42
|
+
# INTERNAL REQUEST WRAPPER
|
|
43
|
+
# ------------------------------------------------------------
|
|
44
|
+
def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
|
|
45
|
+
"""Low-level HTTP request with retry + rate limit handling."""
|
|
46
|
+
|
|
47
|
+
url = f"{self.base_url}{endpoint}"
|
|
48
|
+
|
|
49
|
+
for attempt in range(self.max_retries + 1):
|
|
50
|
+
try:
|
|
51
|
+
response = self.session.request(
|
|
52
|
+
method,
|
|
53
|
+
url,
|
|
54
|
+
timeout=self.timeout,
|
|
55
|
+
**kwargs
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
data = response.json()
|
|
59
|
+
|
|
60
|
+
# -------------------------
|
|
61
|
+
# Successful Response
|
|
62
|
+
# -------------------------
|
|
63
|
+
if response.status_code == 200:
|
|
64
|
+
return data
|
|
65
|
+
|
|
66
|
+
# -------------------------
|
|
67
|
+
# Rate Limited (429)
|
|
68
|
+
# -------------------------
|
|
69
|
+
if response.status_code == 429:
|
|
70
|
+
retry_after = int(response.headers.get("Retry-After", 60))
|
|
71
|
+
if attempt < self.max_retries:
|
|
72
|
+
time.sleep(retry_after)
|
|
73
|
+
continue
|
|
74
|
+
raise MVRApiError(MVRError(**data))
|
|
75
|
+
|
|
76
|
+
# -------------------------
|
|
77
|
+
# Other API Errors
|
|
78
|
+
# -------------------------
|
|
79
|
+
raise MVRApiError(MVRError(**data))
|
|
80
|
+
|
|
81
|
+
except requests.exceptions.RequestException as e:
|
|
82
|
+
# Network errors (timeouts, connection drops)
|
|
83
|
+
if attempt == self.max_retries:
|
|
84
|
+
raise MVRApiError(MVRError(
|
|
85
|
+
ok=False,
|
|
86
|
+
error="NETWORK_ERROR",
|
|
87
|
+
error_code="NETWORK_ERROR",
|
|
88
|
+
message=str(e),
|
|
89
|
+
attribution=AttributionObject(
|
|
90
|
+
framework="Minimum Viable Relationships (MVR)",
|
|
91
|
+
creator="Farouk Mark Mukiibi",
|
|
92
|
+
source="African Market OS",
|
|
93
|
+
license="CC BY 4.0 | Commercial Use Licensed",
|
|
94
|
+
doi="10.5281/zenodo.17310446"
|
|
95
|
+
)
|
|
96
|
+
))
|
|
97
|
+
time.sleep(2 ** attempt) # exponential retry
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------
|
|
100
|
+
# SCORES + SURVEY
|
|
101
|
+
# ------------------------------------------------------------
|
|
102
|
+
def get_scores(self, sector: Optional[str] = None) -> MVRScoreResponse:
|
|
103
|
+
"""GET /v1/scores"""
|
|
104
|
+
params: Dict[str, Any] = {}
|
|
105
|
+
if sector:
|
|
106
|
+
params["sector"] = sector
|
|
107
|
+
|
|
108
|
+
data = self._request("GET", "/v1/scores", params=params)
|
|
109
|
+
return MVRScoreResponse(**data)
|
|
110
|
+
|
|
111
|
+
def survey_aggregate(self, request: SurveyAggregateRequest) -> SurveyAggregateResponse:
|
|
112
|
+
"""POST /v1/survey-aggregate"""
|
|
113
|
+
data = self._request("POST", "/v1/survey-aggregate", json=request.dict())
|
|
114
|
+
return SurveyAggregateResponse(**data)
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------
|
|
117
|
+
# INTELLIGENCE: TRENDS, FORECAST, COMPARE
|
|
118
|
+
# ------------------------------------------------------------
|
|
119
|
+
def get_trends(
|
|
120
|
+
self,
|
|
121
|
+
sector: Optional[str] = None,
|
|
122
|
+
days: Optional[int] = None
|
|
123
|
+
) -> TrendsResponse:
|
|
124
|
+
"""GET /v1/trends"""
|
|
125
|
+
params: Dict[str, Any] = {}
|
|
126
|
+
if sector:
|
|
127
|
+
params["sector"] = sector
|
|
128
|
+
if days is not None:
|
|
129
|
+
params["days"] = days
|
|
130
|
+
|
|
131
|
+
data = self._request("GET", "/v1/trends", params=params)
|
|
132
|
+
return TrendsResponse(**data)
|
|
133
|
+
|
|
134
|
+
def forecast(self, request: ForecastRequest) -> ForecastResponse:
|
|
135
|
+
"""POST /v1/forecast"""
|
|
136
|
+
data = self._request("POST", "/v1/forecast", json=request.dict())
|
|
137
|
+
return ForecastResponse(**data)
|
|
138
|
+
|
|
139
|
+
def compare(self, request: CompareRequest) -> CompareResponse:
|
|
140
|
+
"""POST /v1/compare"""
|
|
141
|
+
data = self._request("POST", "/v1/compare", json=request.dict())
|
|
142
|
+
return CompareResponse(**data)
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------
|
|
145
|
+
# INTELLIGENCE: BENCHMARK, INSIGHTS, TEMPERATURE
|
|
146
|
+
# ------------------------------------------------------------
|
|
147
|
+
def get_benchmark(self, sector: Optional[str] = None) -> BenchmarkResponse:
|
|
148
|
+
"""GET /v1/benchmark"""
|
|
149
|
+
params: Dict[str, Any] = {}
|
|
150
|
+
if sector:
|
|
151
|
+
params["sector"] = sector
|
|
152
|
+
|
|
153
|
+
data = self._request("GET", "/v1/benchmark", params=params)
|
|
154
|
+
return BenchmarkResponse(**data)
|
|
155
|
+
|
|
156
|
+
def get_insights(self, sector: Optional[str] = None) -> InsightsResponse:
|
|
157
|
+
"""GET /v1/insights"""
|
|
158
|
+
params: Dict[str, Any] = {}
|
|
159
|
+
if sector:
|
|
160
|
+
params["sector"] = sector
|
|
161
|
+
|
|
162
|
+
data = self._request("GET", "/v1/insights", params=params)
|
|
163
|
+
return InsightsResponse(**data)
|
|
164
|
+
|
|
165
|
+
def get_temperature(self) -> TemperatureResponse:
|
|
166
|
+
"""GET /v1/temperature"""
|
|
167
|
+
data = self._request("GET", "/v1/temperature")
|
|
168
|
+
return TemperatureResponse(**data)
|
|
169
|
+
|
|
170
|
+
# ------------------------------------------------------------
|
|
171
|
+
# INTELLIGENCE: POLICY + STORY
|
|
172
|
+
# ------------------------------------------------------------
|
|
173
|
+
def get_policy_multi(self) -> PolicyAuditResponse:
|
|
174
|
+
"""GET /v1/policy_multi"""
|
|
175
|
+
data = self._request("GET", "/v1/policy_multi")
|
|
176
|
+
return PolicyAuditResponse(**data)
|
|
177
|
+
|
|
178
|
+
def post_policy_multi(self) -> PolicyAuditResponse:
|
|
179
|
+
"""POST /v1/policy_multi"""
|
|
180
|
+
data = self._request("POST", "/v1/policy_multi")
|
|
181
|
+
return PolicyAuditResponse(**data)
|
|
182
|
+
|
|
183
|
+
def get_story(self) -> StoryResponse:
|
|
184
|
+
"""GET /v1/story"""
|
|
185
|
+
data = self._request("GET", "/v1/story")
|
|
186
|
+
return StoryResponse(**data)
|
|
187
|
+
|
|
188
|
+
def post_story(self) -> StoryResponse:
|
|
189
|
+
"""POST /v1/story"""
|
|
190
|
+
data = self._request("POST", "/v1/story")
|
|
191
|
+
return StoryResponse(**data)
|
|
192
|
+
|
|
193
|
+
# ------------------------------------------------------------
|
|
194
|
+
# UTILITIES
|
|
195
|
+
# ------------------------------------------------------------
|
|
196
|
+
def get_meta(self) -> MetaResponse:
|
|
197
|
+
"""GET /v1/meta"""
|
|
198
|
+
data = self._request("GET", "/v1/meta")
|
|
199
|
+
return MetaResponse(**data)
|
|
200
|
+
|
|
201
|
+
def get_usage(self) -> UsageResponse:
|
|
202
|
+
"""GET /v1/usage"""
|
|
203
|
+
data = self._request("GET", "/v1/usage")
|
|
204
|
+
return UsageResponse(**data)
|
|
205
|
+
|
|
206
|
+
def whoami(self) -> WhoAmIResponse:
|
|
207
|
+
"""GET /v1/whoami"""
|
|
208
|
+
data = self._request("GET", "/v1/whoami")
|
|
209
|
+
return WhoAmIResponse(**data)
|
|
210
|
+
|
|
211
|
+
def get_docs(self) -> DocsResponse:
|
|
212
|
+
"""GET /v1/docs"""
|
|
213
|
+
data = self._request("GET", "/v1/docs")
|
|
214
|
+
return DocsResponse(**data)
|
|
215
|
+
|
|
216
|
+
def create_session(self, license: str, email: str) -> SessionResponse:
|
|
217
|
+
"""POST /v1/session/new"""
|
|
218
|
+
payload = {"license": license, "email": email}
|
|
219
|
+
data = self._request("POST", "/v1/session/new", json=payload)
|
|
220
|
+
return SessionResponse(**data)
|
|
221
|
+
|
|
222
|
+
def health(self) -> HealthResponse:
|
|
223
|
+
"""GET /v1/health"""
|
|
224
|
+
data = self._request("GET", "/v1/health")
|
|
225
|
+
return HealthResponse(**data)
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------------
|
|
228
|
+
# SESSION-BASED CLIENT FACTORY
|
|
229
|
+
# ------------------------------------------------------------
|
|
230
|
+
def with_session(self, session_token: str) -> "SessionMVRApiClient":
|
|
231
|
+
"""Return a SessionMVRApiClient using x-mvr-session auth."""
|
|
232
|
+
return SessionMVRApiClient(
|
|
233
|
+
base_url=self.base_url,
|
|
234
|
+
session_token=session_token,
|
|
235
|
+
timeout=self.timeout
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ============================================================
|
|
240
|
+
# SESSION-BASED CLIENT (x-mvr-session)
|
|
241
|
+
# ============================================================
|
|
242
|
+
class SessionMVRApiClient:
|
|
243
|
+
"""MVR API client using session-token authentication."""
|
|
244
|
+
|
|
245
|
+
def __init__(self, base_url: str, session_token: str, timeout: int = 30):
|
|
246
|
+
self.base_url = base_url
|
|
247
|
+
self.timeout = timeout
|
|
248
|
+
self.session_token = session_token
|
|
249
|
+
|
|
250
|
+
self.session = requests.Session()
|
|
251
|
+
self.session.headers.update({
|
|
252
|
+
"x-mvr-session": session_token,
|
|
253
|
+
"Content-Type": "application/json",
|
|
254
|
+
"User-Agent": "mvr-api-py-client/2.6.0"
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
def _request(self, method: str, endpoint: str, **kwargs) -> Dict[str, Any]:
|
|
258
|
+
"""Internal request handler"""
|
|
259
|
+
url = f"{self.base_url}{endpoint}"
|
|
260
|
+
|
|
261
|
+
response = self.session.request(method, url, timeout=self.timeout, **kwargs)
|
|
262
|
+
data = response.json()
|
|
263
|
+
|
|
264
|
+
if response.status_code != 200:
|
|
265
|
+
raise MVRApiError(MVRError(**data))
|
|
266
|
+
|
|
267
|
+
return data
|
|
268
|
+
|
|
269
|
+
# --------------------------------------------------------
|
|
270
|
+
# Session-auth GET/POST endpoints
|
|
271
|
+
# --------------------------------------------------------
|
|
272
|
+
def get_scores(self, sector: Optional[str] = None) -> MVRScoreResponse:
|
|
273
|
+
params = {"sector": sector} if sector else {}
|
|
274
|
+
data = self._request("GET", "/v1/scores", params=params)
|
|
275
|
+
return MVRScoreResponse(**data)
|
|
276
|
+
|
|
277
|
+
def get_trends(self, sector: Optional[str] = None, days: Optional[int] = None) -> TrendsResponse:
|
|
278
|
+
params = {}
|
|
279
|
+
if sector:
|
|
280
|
+
params["sector"] = sector
|
|
281
|
+
if days:
|
|
282
|
+
params["days"] = days
|
|
283
|
+
|
|
284
|
+
data = self._request("GET", "/v1/trends", params=params)
|
|
285
|
+
return TrendsResponse(**data)
|
|
286
|
+
|
|
287
|
+
def forecast(self, request: ForecastRequest) -> ForecastResponse:
|
|
288
|
+
data = self._request("POST", "/v1/forecast", json=request.dict())
|
|
289
|
+
return ForecastResponse(**data)
|
|
290
|
+
|
|
291
|
+
def compare(self, request: CompareRequest) -> CompareResponse:
|
|
292
|
+
data = self._request("POST", "/v1/compare", json=request.dict())
|
|
293
|
+
return CompareResponse(**data)
|
|
294
|
+
|
|
295
|
+
def get_benchmark(self, sector: Optional[str] = None) -> BenchmarkResponse:
|
|
296
|
+
params = {"sector": sector} if sector else {}
|
|
297
|
+
data = self._request("GET", "/v1/benchmark", params=params)
|
|
298
|
+
return BenchmarkResponse(**data)
|
|
299
|
+
|
|
300
|
+
def get_insights(self, sector: Optional[str] = None) -> InsightsResponse:
|
|
301
|
+
params = {"sector": sector} if sector else {}
|
|
302
|
+
data = self._request("GET", "/v1/insights", params=params)
|
|
303
|
+
return InsightsResponse(**data)
|
|
304
|
+
|
|
305
|
+
def get_temperature(self) -> TemperatureResponse:
|
|
306
|
+
data = self._request("GET", "/v1/temperature")
|
|
307
|
+
return TemperatureResponse(**data)
|
|
308
|
+
|
|
309
|
+
def get_policy_multi(self) -> PolicyAuditResponse:
|
|
310
|
+
data = self._request("GET", "/v1/policy_multi")
|
|
311
|
+
return PolicyAuditResponse(**data)
|
|
312
|
+
|
|
313
|
+
def post_policy_multi(self) -> PolicyAuditResponse:
|
|
314
|
+
data = self._request("POST", "/v1/policy_multi")
|
|
315
|
+
return PolicyAuditResponse(**data)
|
|
316
|
+
|
|
317
|
+
def get_story(self) -> StoryResponse:
|
|
318
|
+
data = self._request("GET", "/v1/story")
|
|
319
|
+
return StoryResponse(**data)
|
|
320
|
+
|
|
321
|
+
def post_story(self) -> StoryResponse:
|
|
322
|
+
data = self._request("POST", "/v1/story")
|
|
323
|
+
return StoryResponse(**data)
|
|
324
|
+
|
|
325
|
+
def get_meta(self) -> MetaResponse:
|
|
326
|
+
data = self._request("GET", "/v1/meta")
|
|
327
|
+
return MetaResponse(**data)
|
|
328
|
+
|
|
329
|
+
def get_usage(self) -> UsageResponse:
|
|
330
|
+
data = self._request("GET", "/v1/usage")
|
|
331
|
+
return UsageResponse(**data)
|
|
332
|
+
|
|
333
|
+
def whoami(self) -> WhoAmIResponse:
|
|
334
|
+
data = self._request("GET", "/v1/whoami")
|
|
335
|
+
return WhoAmIResponse(**data)
|
|
336
|
+
|
|
337
|
+
def get_docs(self) -> DocsResponse:
|
|
338
|
+
data = self._request("GET", "/v1/docs")
|
|
339
|
+
return DocsResponse(**data)
|
|
340
|
+
|
|
341
|
+
def health(self) -> HealthResponse:
|
|
342
|
+
data = self._request("GET", "/v1/health")
|
|
343
|
+
return HealthResponse(**data)
|
|
344
|
+
|
mvr_api/models.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
from typing import List, Optional, Dict, Any, Union
|
|
2
|
+
from pydantic import BaseModel, Field, validator
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AttributionObject(BaseModel):
|
|
7
|
+
framework: str = Field(..., example="Minimum Viable Relationships (MVR)")
|
|
8
|
+
creator: str = Field(..., example="Farouk Mark Mukiibi")
|
|
9
|
+
source: str = Field(..., example="African Market OS")
|
|
10
|
+
license: str = Field(..., example="CC BY 4.0 | Commercial Use Licensed")
|
|
11
|
+
doi: str = Field(..., example="10.5281/zenodo.17310446")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class VersionInfo(BaseModel):
|
|
15
|
+
api: str = Field(..., example="2.6.0")
|
|
16
|
+
feature: Optional[str] = None
|
|
17
|
+
method: Optional[str] = None
|
|
18
|
+
model: Optional[str] = None
|
|
19
|
+
mvr_proprietary: bool = Field(..., example=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MVRError(BaseModel):
|
|
23
|
+
ok: bool = Field(..., example=False)
|
|
24
|
+
error: str
|
|
25
|
+
error_code: str
|
|
26
|
+
message: str
|
|
27
|
+
request_id: Optional[str] = None
|
|
28
|
+
limit: Optional[int] = None
|
|
29
|
+
window: Optional[str] = None
|
|
30
|
+
retry_after: Optional[int] = None
|
|
31
|
+
attribution: AttributionObject
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MVRDimension(BaseModel):
|
|
35
|
+
name: str
|
|
36
|
+
score: float = Field(..., ge=0, le=1)
|
|
37
|
+
confidence: float = Field(..., ge=0, le=1)
|
|
38
|
+
threshold_ok: bool
|
|
39
|
+
binding: bool
|
|
40
|
+
evidence_ptrs: List[str] = Field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MVRScoreResponse(BaseModel):
|
|
44
|
+
ok: bool
|
|
45
|
+
mvr_index: float = Field(..., ge=0, le=1)
|
|
46
|
+
confidence: float = Field(..., ge=0, le=1)
|
|
47
|
+
sector: str
|
|
48
|
+
mvr_dimensions: List[MVRDimension]
|
|
49
|
+
mvr_threshold: bool
|
|
50
|
+
recommendations: List[str]
|
|
51
|
+
version_info: VersionInfo
|
|
52
|
+
attribution: AttributionObject
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class StakeholderResponse(BaseModel):
|
|
56
|
+
dimension: str
|
|
57
|
+
scale: int = Field(..., ge=1, le=5)
|
|
58
|
+
reasons: List[str] = Field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SurveyAggregateRequest(BaseModel):
|
|
62
|
+
stakeholder_responses: List[StakeholderResponse]
|
|
63
|
+
sector: Optional[str] = None
|
|
64
|
+
|
|
65
|
+
@validator("sector")
|
|
66
|
+
def validate_sector(cls, v):
|
|
67
|
+
valid_sectors = [
|
|
68
|
+
"fmcg", "fintech", "policy",
|
|
69
|
+
"health", "agritech", "retail", "default"
|
|
70
|
+
]
|
|
71
|
+
if v and v not in valid_sectors:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Sector must be one of: {', '.join(valid_sectors)}"
|
|
74
|
+
)
|
|
75
|
+
return v
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SurveyAggregateResponse(BaseModel):
|
|
79
|
+
ok: bool
|
|
80
|
+
sector: str
|
|
81
|
+
mvr_index: float = Field(..., ge=0, le=1)
|
|
82
|
+
matrix_axes: Dict[str, float]
|
|
83
|
+
insights: List[str]
|
|
84
|
+
recommendations: List[str]
|
|
85
|
+
summary: str
|
|
86
|
+
version_info: VersionInfo
|
|
87
|
+
attribution: AttributionObject
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TrendsResponse(BaseModel):
|
|
91
|
+
ok: bool
|
|
92
|
+
sector: str
|
|
93
|
+
days: int
|
|
94
|
+
average_index: float
|
|
95
|
+
slope: float
|
|
96
|
+
interpretation: str
|
|
97
|
+
version_info: VersionInfo
|
|
98
|
+
attribution: AttributionObject
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ForecastRequest(BaseModel):
|
|
102
|
+
current_index: float = Field(..., ge=0, le=1)
|
|
103
|
+
velocity: float
|
|
104
|
+
horizon: int = Field(default=30, ge=1)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ForecastResponse(BaseModel):
|
|
108
|
+
ok: bool
|
|
109
|
+
current_index: float = Field(..., ge=0, le=1)
|
|
110
|
+
projected_index: float = Field(..., ge=0, le=1)
|
|
111
|
+
horizon_days: int
|
|
112
|
+
confidence: float = Field(..., ge=0, le=1)
|
|
113
|
+
pmf_projection: str
|
|
114
|
+
version_info: VersionInfo
|
|
115
|
+
attribution: AttributionObject
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class CompareRequest(BaseModel):
|
|
119
|
+
a_index: float = Field(..., ge=0, le=1)
|
|
120
|
+
b_index: float = Field(..., ge=0, le=1)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class CompareResponse(BaseModel):
|
|
124
|
+
ok: bool
|
|
125
|
+
delta: float
|
|
126
|
+
verdict: str
|
|
127
|
+
policy_trace: Dict[str, Any]
|
|
128
|
+
version_info: VersionInfo
|
|
129
|
+
attribution: AttributionObject
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class BenchmarkResponse(BaseModel):
|
|
133
|
+
ok: bool
|
|
134
|
+
sector: str
|
|
135
|
+
benchmark: Dict[str, Union[int, float]]
|
|
136
|
+
version_info: VersionInfo
|
|
137
|
+
attribution: AttributionObject
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class InsightEntity(BaseModel):
|
|
141
|
+
rank: int
|
|
142
|
+
sector: str
|
|
143
|
+
mvr_index: float = Field(..., ge=0, le=1)
|
|
144
|
+
caption: str
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class InsightsResponse(BaseModel):
|
|
148
|
+
ok: bool
|
|
149
|
+
sector: str
|
|
150
|
+
top_entities: List[InsightEntity]
|
|
151
|
+
version_info: VersionInfo
|
|
152
|
+
attribution: AttributionObject
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class TemperatureResponse(BaseModel):
|
|
156
|
+
ok: bool
|
|
157
|
+
date: str
|
|
158
|
+
continent_score: float = Field(..., ge=0, le=1)
|
|
159
|
+
hottest_sector: str
|
|
160
|
+
coolest_sector: str
|
|
161
|
+
region: str
|
|
162
|
+
sample_size: int
|
|
163
|
+
version_info: VersionInfo
|
|
164
|
+
attribution: AttributionObject
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class PolicyAuditResponse(BaseModel):
|
|
168
|
+
ok: bool
|
|
169
|
+
policies_analyzed: int
|
|
170
|
+
compliance_score: float = Field(..., ge=0, le=1)
|
|
171
|
+
recommendations: List[str]
|
|
172
|
+
version_info: VersionInfo
|
|
173
|
+
attribution: AttributionObject
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class StoryResponse(BaseModel):
|
|
177
|
+
ok: bool
|
|
178
|
+
story: str
|
|
179
|
+
impact_metrics: Dict[str, float]
|
|
180
|
+
version_info: VersionInfo
|
|
181
|
+
attribution: AttributionObject
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class MetaResponse(BaseModel):
|
|
185
|
+
ok: bool
|
|
186
|
+
api_name: str
|
|
187
|
+
version: str
|
|
188
|
+
model: str
|
|
189
|
+
endpoints: List[str]
|
|
190
|
+
limits: Dict[str, int]
|
|
191
|
+
last_model_refresh: Optional[str] = None
|
|
192
|
+
model_fingerprint: Optional[str] = None
|
|
193
|
+
attribution: AttributionObject
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class UsageResponse(BaseModel):
|
|
197
|
+
ok: bool
|
|
198
|
+
plan: str
|
|
199
|
+
date: str
|
|
200
|
+
used_today: int
|
|
201
|
+
daily_limit: int
|
|
202
|
+
attribution: AttributionObject
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class WhoAmIResponse(BaseModel):
|
|
206
|
+
ok: bool
|
|
207
|
+
api: str
|
|
208
|
+
version: str
|
|
209
|
+
capabilities: List[str]
|
|
210
|
+
author: str
|
|
211
|
+
region: str
|
|
212
|
+
attribution: AttributionObject
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class DocsResponse(BaseModel):
|
|
216
|
+
ok: bool
|
|
217
|
+
documentation: str
|
|
218
|
+
endpoints: List[str]
|
|
219
|
+
version_info: VersionInfo
|
|
220
|
+
attribution: AttributionObject
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class SessionResponse(BaseModel):
|
|
224
|
+
ok: bool
|
|
225
|
+
session_token: str
|
|
226
|
+
expires_in: int
|
|
227
|
+
expires_at: str
|
|
228
|
+
plan: str
|
|
229
|
+
version_info: VersionInfo
|
|
230
|
+
attribution: AttributionObject
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class HealthResponse(BaseModel):
|
|
234
|
+
ok: bool
|
|
235
|
+
service: str
|
|
236
|
+
time: str
|
|
237
|
+
region: str
|
|
238
|
+
performance: str
|
|
239
|
+
security: str
|
|
240
|
+
version: str
|
|
241
|
+
uptime_seconds: int
|
|
242
|
+
features: List[str]
|
|
243
|
+
attribution: AttributionObject
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class MVRApiConfig(BaseModel):
|
|
247
|
+
license: str
|
|
248
|
+
email: str
|
|
249
|
+
base_url: str = "https://mvr-api.africanmarketos.workers.dev"
|
|
250
|
+
timeout: int = 30
|
|
251
|
+
max_retries: int = 3
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mvr-api-client
|
|
3
|
+
Version: 2.6.0
|
|
4
|
+
Summary: Python client for African Market OS — Minimum Viable Relationships (MVR) API
|
|
5
|
+
Home-page: https://github.com/africanmarketos/mvr-api-py-client
|
|
6
|
+
Author: African Market OS
|
|
7
|
+
Author-email: African Market OS <info@africanmarketos.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Project-URL: Homepage, https://github.com/africanmarketos/mvr-api-py-client
|
|
10
|
+
Project-URL: Repository, https://github.com/africanmarketos/mvr-api-py-client
|
|
11
|
+
Project-URL: Issues, https://github.com/africanmarketos/mvr-api-py-client/issues
|
|
12
|
+
Keywords: mvr,api,relationships,african-market,trust
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: requests>=2.25.0
|
|
17
|
+
Requires-Dist: pydantic>=1.8.0
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
Dynamic: requires-python
|
|
22
|
+
|
|
23
|
+
# African Market OS — MVR API Python Client
|
|
24
|
+
|
|
25
|
+
Official **Python SDK** for the
|
|
26
|
+
**Minimum Viable Relationships (MVR) API — v2.6.0-enterprise**
|
|
27
|
+
|
|
28
|
+
This client provides full access to all MVR API endpoints:
|
|
29
|
+
|
|
30
|
+
### ✔ Scores
|
|
31
|
+
### ✔ Survey aggregation
|
|
32
|
+
### ✔ Trends
|
|
33
|
+
### ✔ Forecasts
|
|
34
|
+
### ✔ Benchmarking
|
|
35
|
+
### ✔ Insights
|
|
36
|
+
### ✔ Policy multi-audit
|
|
37
|
+
### ✔ Stories
|
|
38
|
+
### ✔ Metadata + Usage
|
|
39
|
+
### ✔ Health checks
|
|
40
|
+
### ✔ Session token authentication
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 📦 Installation
|
|
45
|
+
|
|
46
|
+
You can install directly from PyPI (recommended):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install mvr-api-client
|
|
50
|
+
|
|
51
|
+
Or install from source:
|
|
52
|
+
|
|
53
|
+
pip install .
|
|
54
|
+
|
|
55
|
+
🚀 Quickstart Example
|
|
56
|
+
|
|
57
|
+
from mvr_api import MVRApiClient, MVRApiConfig
|
|
58
|
+
|
|
59
|
+
# Create configuration
|
|
60
|
+
config = MVRApiConfig(
|
|
61
|
+
license="your-license-key",
|
|
62
|
+
email="you@example.com"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Initialize client
|
|
66
|
+
client = MVRApiClient(config)
|
|
67
|
+
|
|
68
|
+
# Call API
|
|
69
|
+
scores = client.get_scores("fintech")
|
|
70
|
+
print("MVR Index:", scores.mvr_index)
|
|
71
|
+
|
|
72
|
+
🧪 Submitting Survey Data
|
|
73
|
+
|
|
74
|
+
from mvr_api import SurveyAggregateRequest, StakeholderResponse
|
|
75
|
+
|
|
76
|
+
survey_request = SurveyAggregateRequest(
|
|
77
|
+
stakeholder_responses=[
|
|
78
|
+
StakeholderResponse(
|
|
79
|
+
dimension="Embeddedness",
|
|
80
|
+
scale=4,
|
|
81
|
+
reasons=["Strong community integration"]
|
|
82
|
+
)
|
|
83
|
+
],
|
|
84
|
+
sector="fintech"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
result = client.survey_aggregate(survey_request)
|
|
88
|
+
print(result.mvr_index)
|
|
89
|
+
|
|
90
|
+
📈 Trends Example
|
|
91
|
+
trends = client.get_trends(sector="fmcg", days=30)
|
|
92
|
+
print("Average Index:", trends.average_index)
|
|
93
|
+
print("Slope:", trends.slope)
|
|
94
|
+
|
|
95
|
+
🔮 Forecast Example
|
|
96
|
+
from mvr_api import ForecastRequest
|
|
97
|
+
|
|
98
|
+
forecast = client.forecast(ForecastRequest(
|
|
99
|
+
current_index=0.65,
|
|
100
|
+
velocity=0.02,
|
|
101
|
+
horizon=30
|
|
102
|
+
))
|
|
103
|
+
|
|
104
|
+
print("Projected MVR:", forecast.projected_index)
|
|
105
|
+
|
|
106
|
+
👥 Entity Comparison
|
|
107
|
+
from mvr_api import CompareRequest
|
|
108
|
+
|
|
109
|
+
comparison = client.compare(CompareRequest(
|
|
110
|
+
a_index=0.72,
|
|
111
|
+
b_index=0.58
|
|
112
|
+
))
|
|
113
|
+
|
|
114
|
+
print("Delta:", comparison.delta)
|
|
115
|
+
print("Verdict:", comparison.verdict)
|
|
116
|
+
|
|
117
|
+
📊 Benchmarks
|
|
118
|
+
bench = client.get_benchmark("fintech")
|
|
119
|
+
print(bench.benchmark)
|
|
120
|
+
|
|
121
|
+
♨ Temperature
|
|
122
|
+
temp = client.get_temperature()
|
|
123
|
+
print(temp.continent_score)
|
|
124
|
+
|
|
125
|
+
📘 Metadata
|
|
126
|
+
meta = client.get_meta()
|
|
127
|
+
print(meta.model)
|
|
128
|
+
|
|
129
|
+
🔐 Session-Based Authentication
|
|
130
|
+
# Create session token
|
|
131
|
+
session = client.create_session("license-key", "you@example.com")
|
|
132
|
+
|
|
133
|
+
# Build session-authenticated client
|
|
134
|
+
session_client = client.with_session(session.session_token)
|
|
135
|
+
|
|
136
|
+
scores = session_client.get_scores()
|
|
137
|
+
print(scores.mvr_index)
|
|
138
|
+
|
|
139
|
+
🛡 Error Handling
|
|
140
|
+
|
|
141
|
+
All API errors raise a structured MVRApiError:
|
|
142
|
+
|
|
143
|
+
from mvr_api import MVRApiError
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
client.get_scores()
|
|
147
|
+
except MVRApiError as e:
|
|
148
|
+
print("Error:", e.error_data.error_code)
|
|
149
|
+
print("Message:", e.error_data.message)
|
|
150
|
+
|
|
151
|
+
📂 Project Structure
|
|
152
|
+
mvr-api-py-client/
|
|
153
|
+
│
|
|
154
|
+
├── setup.py
|
|
155
|
+
├── README.md
|
|
156
|
+
└── mvr_api/
|
|
157
|
+
├── __init__.py
|
|
158
|
+
├── client.py
|
|
159
|
+
└── models.py
|
|
160
|
+
|
|
161
|
+
📄 License
|
|
162
|
+
|
|
163
|
+
This SDK is released under the MIT License.
|
|
164
|
+
|
|
165
|
+
🧬 Attribution
|
|
166
|
+
MVR Framework • African Market OS
|
|
167
|
+
Creator: Farouk Mark Mukiibi
|
|
168
|
+
Framework DOI: 10.5281/zenodo.17310446
|
|
169
|
+
|
|
170
|
+
🌍 About
|
|
171
|
+
|
|
172
|
+
The Minimum Viable Relationships (MVR) Framework measures:
|
|
173
|
+
|
|
174
|
+
Trust
|
|
175
|
+
|
|
176
|
+
Belonging
|
|
177
|
+
|
|
178
|
+
Permission
|
|
179
|
+
|
|
180
|
+
Embeddedness
|
|
181
|
+
|
|
182
|
+
…to evaluate relational readiness for ventures entering
|
|
183
|
+
high-context markets across Africa.
|
|
184
|
+
|
|
185
|
+
Learn more:https://africanmarketos.com/the-mvr-framework-minimum-viable-relationships/
|
|
186
|
+
|
|
187
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mvr_api/__init__.py,sha256=7ZWR7KSHMIRC7jRnKBOuljNd9sfVM2ZxRmBt47Ax2uw,1219
|
|
2
|
+
mvr_api/client.py,sha256=cqN8EUW0PigatK7GgyKDXzFsiJ5rue26KqlzpG6Dg_M,13010
|
|
3
|
+
mvr_api/models.py,sha256=LZH36C61Q0o3BXzrtN9MRYCK3WeVt7dlkCSmeqwb13k,5978
|
|
4
|
+
mvr_api_client-2.6.0.dist-info/licenses/LICENSE,sha256=gEdYkTY_4fN-0q2d1j72N1pxtN_OYstZHIzTP79HNGg,1074
|
|
5
|
+
mvr_api_client-2.6.0.dist-info/METADATA,sha256=vZm7BmisQwKEf_S89PLCuX37t4849AON3KmYEnfPtHk,4175
|
|
6
|
+
mvr_api_client-2.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
mvr_api_client-2.6.0.dist-info/top_level.txt,sha256=XtH_FTPIizqk666Wo2PX7ueinRNcwoZApH16IxXtcDA,8
|
|
8
|
+
mvr_api_client-2.6.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 African Market OS
|
|
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 @@
|
|
|
1
|
+
mvr_api
|