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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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