metrify-sdk 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.
- metrify/__init__.py +20 -0
- metrify/billing.py +85 -0
- metrify/client.py +47 -0
- metrify/decorator.py +65 -0
- metrify/exceptions.py +37 -0
- metrify/models.py +25 -0
- metrify_sdk-0.1.0.dist-info/METADATA +176 -0
- metrify_sdk-0.1.0.dist-info/RECORD +10 -0
- metrify_sdk-0.1.0.dist-info/WHEEL +5 -0
- metrify_sdk-0.1.0.dist-info/top_level.txt +1 -0
metrify/__init__.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from metrify.client import Metrify
|
|
2
|
+
from metrify.exceptions import (
|
|
3
|
+
ConsumerSuspendedError,
|
|
4
|
+
GatewayError,
|
|
5
|
+
InsufficientBalanceError,
|
|
6
|
+
ProviderNotFoundError,
|
|
7
|
+
ProviderSuspendedError,
|
|
8
|
+
UpstreamError,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Metrify",
|
|
13
|
+
"UpstreamError",
|
|
14
|
+
"InsufficientBalanceError",
|
|
15
|
+
"ProviderSuspendedError",
|
|
16
|
+
"ConsumerSuspendedError",
|
|
17
|
+
"ProviderNotFoundError",
|
|
18
|
+
"GatewayError",
|
|
19
|
+
]
|
|
20
|
+
__version__ = "0.1.0"
|
metrify/billing.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
|
|
3
|
+
from metrify.exceptions import (
|
|
4
|
+
ConsumerSuspendedError,
|
|
5
|
+
GatewayError,
|
|
6
|
+
InsufficientBalanceError,
|
|
7
|
+
ProviderNotFoundError,
|
|
8
|
+
ProviderSuspendedError,
|
|
9
|
+
)
|
|
10
|
+
from metrify.models import ChargeRequest, ChargeResponse
|
|
11
|
+
|
|
12
|
+
_ERROR_MAP = {
|
|
13
|
+
"INSUFFICIENT_BALANCE": InsufficientBalanceError,
|
|
14
|
+
"PROVIDER_SUSPENDED": ProviderSuspendedError,
|
|
15
|
+
"CONSUMER_SUSPENDED": ConsumerSuspendedError,
|
|
16
|
+
"PROVIDER_NOT_FOUND": ProviderNotFoundError,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
_TIMEOUT = 10.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BillingClient:
|
|
23
|
+
def __init__(self, gateway_url: str, provider_key: str) -> None:
|
|
24
|
+
self._gateway_url = gateway_url.rstrip("/")
|
|
25
|
+
self._provider_key = provider_key
|
|
26
|
+
|
|
27
|
+
def check_balance(self, consumer_api_key: str, required: float) -> None:
|
|
28
|
+
"""Verify consumer has at least `required` balance without charging.
|
|
29
|
+
|
|
30
|
+
Raises InsufficientBalanceError or GatewayError on failure.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
with httpx.Client(timeout=_TIMEOUT) as client:
|
|
34
|
+
response = client.get(
|
|
35
|
+
f"{self._gateway_url}/consumers/me",
|
|
36
|
+
headers={"X-API-Key": consumer_api_key},
|
|
37
|
+
)
|
|
38
|
+
except (httpx.TimeoutException, httpx.NetworkError, httpx.TransportError) as exc:
|
|
39
|
+
raise GatewayError(f"Request to billing gateway failed: {exc}") from exc
|
|
40
|
+
|
|
41
|
+
if response.status_code >= 500:
|
|
42
|
+
raise GatewayError(f"Billing gateway returned {response.status_code}")
|
|
43
|
+
|
|
44
|
+
if not response.is_success:
|
|
45
|
+
self._raise_api_error(response)
|
|
46
|
+
|
|
47
|
+
balance = float(response.json().get("balance", 0.0))
|
|
48
|
+
if balance < required:
|
|
49
|
+
raise InsufficientBalanceError(
|
|
50
|
+
f"Consumer balance {balance:.4f} is insufficient for cost {required:.4f}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def charge(self, consumer_api_key: str, tool_name: str, cost: float) -> ChargeResponse:
|
|
54
|
+
request = ChargeRequest(
|
|
55
|
+
provider_key=self._provider_key,
|
|
56
|
+
tool_name=tool_name,
|
|
57
|
+
cost=cost,
|
|
58
|
+
)
|
|
59
|
+
try:
|
|
60
|
+
with httpx.Client(timeout=_TIMEOUT) as client:
|
|
61
|
+
response = client.post(
|
|
62
|
+
f"{self._gateway_url}/billing/charge",
|
|
63
|
+
json=request.model_dump(),
|
|
64
|
+
headers={"X-API-Key": consumer_api_key},
|
|
65
|
+
)
|
|
66
|
+
except (httpx.TimeoutException, httpx.NetworkError, httpx.TransportError) as exc:
|
|
67
|
+
raise GatewayError(f"Request to billing gateway failed: {exc}") from exc
|
|
68
|
+
|
|
69
|
+
if response.status_code >= 500:
|
|
70
|
+
raise GatewayError(f"Billing gateway returned {response.status_code}")
|
|
71
|
+
|
|
72
|
+
if not response.is_success:
|
|
73
|
+
self._raise_api_error(response)
|
|
74
|
+
|
|
75
|
+
return ChargeResponse.model_validate(response.json())
|
|
76
|
+
|
|
77
|
+
def _raise_api_error(self, response: httpx.Response) -> None:
|
|
78
|
+
try:
|
|
79
|
+
body = response.json()
|
|
80
|
+
error_code = body.get("error") or body.get("code") or ""
|
|
81
|
+
except Exception:
|
|
82
|
+
error_code = ""
|
|
83
|
+
|
|
84
|
+
exc_class = _ERROR_MAP.get(error_code, GatewayError)
|
|
85
|
+
raise exc_class(f"Billing gateway error {response.status_code}: {error_code}")
|
metrify/client.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Callable, Optional
|
|
3
|
+
|
|
4
|
+
from dotenv import load_dotenv
|
|
5
|
+
|
|
6
|
+
from metrify.billing import BillingClient
|
|
7
|
+
from metrify.decorator import make_tool_decorator
|
|
8
|
+
from metrify.models import BillingUnit
|
|
9
|
+
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
_DEFAULT_GATEWAY_URL = "https://airy-wholeness-production-fcc4.up.railway.app"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Metrify:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
provider_key: Optional[str] = None,
|
|
19
|
+
gateway_url: Optional[str] = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
resolved_key = provider_key or os.environ.get("METRIFY_PROVIDER_KEY")
|
|
22
|
+
if not resolved_key:
|
|
23
|
+
raise ValueError(
|
|
24
|
+
"provider_key is required. Pass it to Metrify(provider_key=...) "
|
|
25
|
+
"or set the METRIFY_PROVIDER_KEY environment variable."
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
resolved_url = (
|
|
29
|
+
gateway_url
|
|
30
|
+
or os.environ.get("METRIFY_GATEWAY_URL")
|
|
31
|
+
or _DEFAULT_GATEWAY_URL
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
self._provider_key = resolved_key
|
|
35
|
+
self._gateway_url = resolved_url
|
|
36
|
+
self._billing = BillingClient(gateway_url=resolved_url, provider_key=resolved_key)
|
|
37
|
+
|
|
38
|
+
def tool(self, price: float, unit: BillingUnit = "per_call") -> Callable:
|
|
39
|
+
def decorator(func: Callable) -> Callable:
|
|
40
|
+
return make_tool_decorator(
|
|
41
|
+
billing=self._billing,
|
|
42
|
+
price=price,
|
|
43
|
+
unit=unit,
|
|
44
|
+
tool_name=func.__name__,
|
|
45
|
+
)(func)
|
|
46
|
+
|
|
47
|
+
return decorator
|
metrify/decorator.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from metrify.billing import BillingClient
|
|
6
|
+
from metrify.exceptions import GatewayError, InsufficientBalanceError, UpstreamError
|
|
7
|
+
from metrify.models import BillingUnit
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _extract_consumer_key(args: tuple, kwargs: dict, func: Callable) -> str:
|
|
11
|
+
if "consumer_api_key" in kwargs:
|
|
12
|
+
return kwargs["consumer_api_key"]
|
|
13
|
+
|
|
14
|
+
sig = inspect.signature(func)
|
|
15
|
+
params = list(sig.parameters.keys())
|
|
16
|
+
if params and params[0] == "consumer_api_key" and args:
|
|
17
|
+
return args[0]
|
|
18
|
+
|
|
19
|
+
raise TypeError(
|
|
20
|
+
f"{func.__name__}() missing required argument 'consumer_api_key'. "
|
|
21
|
+
"Pass it as the first positional argument or as a keyword argument."
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_tool_decorator(
|
|
26
|
+
billing: BillingClient,
|
|
27
|
+
price: float,
|
|
28
|
+
unit: BillingUnit,
|
|
29
|
+
tool_name: str,
|
|
30
|
+
) -> Callable:
|
|
31
|
+
def decorator(func: Callable) -> Callable:
|
|
32
|
+
if inspect.iscoroutinefunction(func):
|
|
33
|
+
@functools.wraps(func)
|
|
34
|
+
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
35
|
+
consumer_api_key = _extract_consumer_key(args, kwargs, func)
|
|
36
|
+
billing.check_balance(consumer_api_key=consumer_api_key, required=price)
|
|
37
|
+
try:
|
|
38
|
+
result = await func(*args, **kwargs)
|
|
39
|
+
except UpstreamError as exc:
|
|
40
|
+
return str(exc)
|
|
41
|
+
except (InsufficientBalanceError, GatewayError):
|
|
42
|
+
raise
|
|
43
|
+
except Exception as exc:
|
|
44
|
+
return f"Tool error: {exc}"
|
|
45
|
+
billing.charge(consumer_api_key=consumer_api_key, tool_name=tool_name, cost=price)
|
|
46
|
+
return result
|
|
47
|
+
return async_wrapper
|
|
48
|
+
else:
|
|
49
|
+
@functools.wraps(func)
|
|
50
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
51
|
+
consumer_api_key = _extract_consumer_key(args, kwargs, func)
|
|
52
|
+
billing.check_balance(consumer_api_key=consumer_api_key, required=price)
|
|
53
|
+
try:
|
|
54
|
+
result = func(*args, **kwargs)
|
|
55
|
+
except UpstreamError as exc:
|
|
56
|
+
return str(exc)
|
|
57
|
+
except (InsufficientBalanceError, GatewayError):
|
|
58
|
+
raise
|
|
59
|
+
except Exception as exc:
|
|
60
|
+
return f"Tool error: {exc}"
|
|
61
|
+
billing.charge(consumer_api_key=consumer_api_key, tool_name=tool_name, cost=price)
|
|
62
|
+
return result
|
|
63
|
+
return sync_wrapper
|
|
64
|
+
|
|
65
|
+
return decorator
|
metrify/exceptions.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
class MetrifyError(Exception):
|
|
2
|
+
"""Base exception for all metrify SDK errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class InsufficientBalanceError(MetrifyError):
|
|
6
|
+
"""Consumer does not have enough balance to pay for the tool call."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ProviderSuspendedError(MetrifyError):
|
|
10
|
+
"""The provider account has been suspended."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ConsumerSuspendedError(MetrifyError):
|
|
14
|
+
"""The consumer account has been suspended."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ProviderNotFoundError(MetrifyError):
|
|
18
|
+
"""The provider key is invalid or not registered."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GatewayError(MetrifyError):
|
|
22
|
+
"""Network error, timeout, or backend returned a 5xx response."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class UpstreamError(MetrifyError):
|
|
26
|
+
"""Raised by a tool when an upstream/external API fails.
|
|
27
|
+
|
|
28
|
+
The decorator catches this, skips billing, and returns the message to the
|
|
29
|
+
consumer as a formatted error string.
|
|
30
|
+
|
|
31
|
+
Example::
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
result = await anthropic_client.complete(...)
|
|
35
|
+
except anthropic.APIError as e:
|
|
36
|
+
raise UpstreamError(f"Anthropic API error: {e.message}")
|
|
37
|
+
"""
|
metrify/models.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
BillingUnit = Literal["per_call", "per_token", "per_minute", "per_image", "per_page"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ChargeRequest(BaseModel):
|
|
8
|
+
provider_key: str
|
|
9
|
+
tool_name: str
|
|
10
|
+
cost: float
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChargeResponse(BaseModel):
|
|
14
|
+
transaction_id: str
|
|
15
|
+
status: str
|
|
16
|
+
cost: float
|
|
17
|
+
provider_share: float
|
|
18
|
+
metrify_fee: float
|
|
19
|
+
consumer_balance_after: float
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ToolConfig(BaseModel):
|
|
23
|
+
name: str
|
|
24
|
+
price: float
|
|
25
|
+
unit: BillingUnit
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: metrify-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Billing SDK for MCP servers
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/mangelico/metrify-sdk
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: httpx>=0.27.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0
|
|
11
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
15
|
+
Requires-Dist: pytest-mock>=3.12; extra == "dev"
|
|
16
|
+
Requires-Dist: respx>=0.21; extra == "dev"
|
|
17
|
+
|
|
18
|
+
# metrify
|
|
19
|
+
|
|
20
|
+
**Billing SDK for MCP servers.** Add pay-per-call monetization to any MCP tool
|
|
21
|
+
in under 5 minutes — no payment infrastructure required.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
### 1. Register as a provider
|
|
28
|
+
|
|
29
|
+
Sign up at the Metrify dashboard and get your `pk_live_...` provider key.
|
|
30
|
+
|
|
31
|
+
### 2. Install the SDK
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install metrify
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 3. Configure your provider key
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
export METRIFY_PROVIDER_KEY="pk_live_your_key_here"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or pass it directly in code (see step 4).
|
|
44
|
+
|
|
45
|
+
### 4. Decorate your tools
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from metrify import Metrify
|
|
49
|
+
|
|
50
|
+
m = Metrify() # reads METRIFY_PROVIDER_KEY from env
|
|
51
|
+
|
|
52
|
+
@m.tool(price=0.01, unit="per_call")
|
|
53
|
+
async def summarize(consumer_api_key: str, text: str) -> str:
|
|
54
|
+
# Balance is pre-checked before this runs; charge happens only on success
|
|
55
|
+
return f"Summary of: {text[:50]}..."
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 5. Deploy
|
|
59
|
+
|
|
60
|
+
Your tool now charges consumers on every call. The `consumer_api_key` is
|
|
61
|
+
extracted automatically by the decorator — consumers pass it as the first
|
|
62
|
+
argument or as a keyword argument.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Full example with FastMCP
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from metrify import Metrify
|
|
70
|
+
from mcp.server.fastmcp import FastMCP
|
|
71
|
+
|
|
72
|
+
m = Metrify(provider_key="pk_live_your_key_here")
|
|
73
|
+
server = FastMCP("my-mcp-server")
|
|
74
|
+
|
|
75
|
+
@server.tool()
|
|
76
|
+
@m.tool(price=0.05, unit="per_call")
|
|
77
|
+
async def analyze_document(consumer_api_key: str, content: str) -> str:
|
|
78
|
+
"""Analyze a document and return insights."""
|
|
79
|
+
# billing.charge() runs before this line
|
|
80
|
+
analysis = await run_analysis(content)
|
|
81
|
+
return analysis
|
|
82
|
+
|
|
83
|
+
@server.tool()
|
|
84
|
+
@m.tool(price=0.001, unit="per_token")
|
|
85
|
+
async def generate_text(consumer_api_key: str, prompt: str) -> str:
|
|
86
|
+
"""Generate text from a prompt."""
|
|
87
|
+
result = await llm.complete(prompt)
|
|
88
|
+
return result
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Billing flow
|
|
94
|
+
|
|
95
|
+
The decorator runs a **two-phase billing** flow on every call:
|
|
96
|
+
|
|
97
|
+
1. **Pre-check** — verify the consumer has enough balance (no charge yet).
|
|
98
|
+
2. **Execute** — run your tool function.
|
|
99
|
+
3. **Charge** — deduct only if the function completed without error.
|
|
100
|
+
|
|
101
|
+
If the function raises any exception the consumer is never charged.
|
|
102
|
+
|
|
103
|
+
## Exception reference
|
|
104
|
+
|
|
105
|
+
| Exception | Raised by | Behavior |
|
|
106
|
+
|-----------|-----------|----------|
|
|
107
|
+
| `InsufficientBalanceError` | Pre-check | Propagated — consumer has no funds |
|
|
108
|
+
| `UpstreamError` | Your tool | Caught — returns message to consumer, no charge |
|
|
109
|
+
| `ProviderSuspendedError` | Pre-check / charge | Propagated — your account is suspended |
|
|
110
|
+
| `ConsumerSuspendedError` | Pre-check / charge | Propagated — consumer account is suspended |
|
|
111
|
+
| `ProviderNotFoundError` | Pre-check / charge | Propagated — your provider key is invalid |
|
|
112
|
+
| `GatewayError` | Pre-check / charge | Propagated — network error or backend 5xx |
|
|
113
|
+
| Any other `Exception` | Your tool | Caught — returns `"Tool error: ..."`, no charge |
|
|
114
|
+
|
|
115
|
+
### Signalling upstream failures with `UpstreamError`
|
|
116
|
+
|
|
117
|
+
Use `UpstreamError` when an external API your tool depends on fails. The
|
|
118
|
+
decorator catches it, skips billing, and returns your message to the consumer:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from metrify import Metrify, UpstreamError
|
|
122
|
+
|
|
123
|
+
m = Metrify()
|
|
124
|
+
|
|
125
|
+
@m.tool(price=0.05, unit="per_call")
|
|
126
|
+
async def summarize(consumer_api_key: str, text: str) -> str:
|
|
127
|
+
try:
|
|
128
|
+
result = await anthropic_client.messages.create(...)
|
|
129
|
+
except anthropic.APIError as e:
|
|
130
|
+
raise UpstreamError(f"Anthropic API error: {e.message}")
|
|
131
|
+
return result.content[0].text
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The consumer receives `"Anthropic API error: ..."` and is not charged.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from metrify.exceptions import InsufficientBalanceError, GatewayError
|
|
138
|
+
|
|
139
|
+
# In your MCP server error handler:
|
|
140
|
+
try:
|
|
141
|
+
result = await my_tool(consumer_api_key=ck, query=q)
|
|
142
|
+
except InsufficientBalanceError:
|
|
143
|
+
return "Insufficient balance. Please top up at the Metrify dashboard."
|
|
144
|
+
except GatewayError:
|
|
145
|
+
return "Billing service temporarily unavailable. Please retry."
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Environment variables
|
|
151
|
+
|
|
152
|
+
| Variable | Default | Description |
|
|
153
|
+
|----------|---------|-------------|
|
|
154
|
+
| `METRIFY_PROVIDER_KEY` | — | Your provider key (`pk_live_...`) |
|
|
155
|
+
| `METRIFY_GATEWAY_URL` | `https://airy-wholeness-production-fcc4.up.railway.app` | Override backend URL |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Supported billing units (V1)
|
|
160
|
+
|
|
161
|
+
All units charge a flat `price` per call. The `unit` field is displayed in your
|
|
162
|
+
dashboard for reporting purposes.
|
|
163
|
+
|
|
164
|
+
| Unit | Use case |
|
|
165
|
+
|------|----------|
|
|
166
|
+
| `per_call` | Fixed price per tool invocation |
|
|
167
|
+
| `per_token` | LLM completions (flat in V1) |
|
|
168
|
+
| `per_minute` | Audio/video processing |
|
|
169
|
+
| `per_image` | Image generation or analysis |
|
|
170
|
+
| `per_page` | Document processing |
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
metrify/__init__.py,sha256=LkFYq9U0TIJbdOm_HJ7wYd4azvvM51kuX4spIbQR06M,434
|
|
2
|
+
metrify/billing.py,sha256=iS-5MQYd2Htsb3Y4WcGUjsIgqml6q-D8QlcW4S9i5J4,3129
|
|
3
|
+
metrify/client.py,sha256=X_F6pL0srSIi75Zy93LVAGaAv2fi83l9KfLcsQStOVM,1439
|
|
4
|
+
metrify/decorator.py,sha256=WMTrlD6bio5hhzawPdzlQ6wVAz6kdyUIXDnPPi2YznU,2522
|
|
5
|
+
metrify/exceptions.py,sha256=XgNYGpMkUjCAPNT8jz66XDC9JDZwjYCpG8iImTvkLek,1031
|
|
6
|
+
metrify/models.py,sha256=_UYhZ_ge32j4oW_f5-YTE8SnCFchL0LTGCnAwny-ZGo,494
|
|
7
|
+
metrify_sdk-0.1.0.dist-info/METADATA,sha256=f46sjpS6jX9ff2-mRTO6Nw044tEaOeOy4Zwshs0RtCk,5268
|
|
8
|
+
metrify_sdk-0.1.0.dist-info/WHEEL,sha256=BNRMDyzLkkcmlv0J8ppDQkk2VED33SesJDynr9ED1gc,91
|
|
9
|
+
metrify_sdk-0.1.0.dist-info/top_level.txt,sha256=aMBu6UysoY7pXDQfpfXLpTWXGV0tOgnX95Ha8t0tOFY,8
|
|
10
|
+
metrify_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
metrify
|