milligold-mcp 0.1.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.
- milligold_mcp-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +42 -0
- milligold_mcp-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
- milligold_mcp-0.1.0/.github/banner.png +0 -0
- milligold_mcp-0.1.0/.github/dependabot.yml +10 -0
- milligold_mcp-0.1.0/.github/workflows/ci.yml +22 -0
- milligold_mcp-0.1.0/.github/workflows/publish.yml +51 -0
- milligold_mcp-0.1.0/.gitignore +6 -0
- milligold_mcp-0.1.0/LICENSE +21 -0
- milligold_mcp-0.1.0/PKG-INFO +76 -0
- milligold_mcp-0.1.0/README.md +50 -0
- milligold_mcp-0.1.0/pyproject.toml +54 -0
- milligold_mcp-0.1.0/server.json +32 -0
- milligold_mcp-0.1.0/src/milligold_mcp/__init__.py +5 -0
- milligold_mcp-0.1.0/src/milligold_mcp/gold.py +110 -0
- milligold_mcp-0.1.0/src/milligold_mcp/http.py +220 -0
- milligold_mcp-0.1.0/src/milligold_mcp/registry.py +16 -0
- milligold_mcp-0.1.0/src/milligold_mcp/server.py +84 -0
- milligold_mcp-0.1.0/tests/conftest.py +72 -0
- milligold_mcp-0.1.0/tests/fixtures/price.json +1 -0
- milligold_mcp-0.1.0/tests/fixtures/report.json +1 -0
- milligold_mcp-0.1.0/tests/fixtures/widget.json +1 -0
- milligold_mcp-0.1.0/tests/test_gold.py +93 -0
- milligold_mcp-0.1.0/tests/test_live_gold.py +19 -0
- milligold_mcp-0.1.0/uv.lock +1292 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: A tool failed or returned wrong data
|
|
3
|
+
labels: [bug]
|
|
4
|
+
body:
|
|
5
|
+
- type: input
|
|
6
|
+
id: tool
|
|
7
|
+
attributes:
|
|
8
|
+
label: Tool
|
|
9
|
+
description: Which tool, e.g. tj_get_prices
|
|
10
|
+
validations:
|
|
11
|
+
required: true
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: input
|
|
14
|
+
attributes:
|
|
15
|
+
label: Arguments and result
|
|
16
|
+
description: The arguments the agent sent and the error or wrong output. Remove anything personal.
|
|
17
|
+
render: json
|
|
18
|
+
validations:
|
|
19
|
+
required: true
|
|
20
|
+
- type: textarea
|
|
21
|
+
id: expected
|
|
22
|
+
attributes:
|
|
23
|
+
label: What you expected
|
|
24
|
+
description: e.g. the price shown on milli.gold
|
|
25
|
+
- type: input
|
|
26
|
+
id: client
|
|
27
|
+
attributes:
|
|
28
|
+
label: MCP client and OS
|
|
29
|
+
placeholder: Claude Desktop 1.x on Windows 11
|
|
30
|
+
- type: dropdown
|
|
31
|
+
id: network
|
|
32
|
+
attributes:
|
|
33
|
+
label: Network
|
|
34
|
+
options:
|
|
35
|
+
- Direct connection
|
|
36
|
+
- VPN or proxy
|
|
37
|
+
- MILLIGOLD_MCP_PROXY set
|
|
38
|
+
- type: input
|
|
39
|
+
id: version
|
|
40
|
+
attributes:
|
|
41
|
+
label: milligold-mcp version
|
|
42
|
+
placeholder: "0.1.0"
|
|
Binary file
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v7
|
|
16
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: uv sync --locked
|
|
20
|
+
- run: uv run ruff check .
|
|
21
|
+
- run: uv run ruff format --check .
|
|
22
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Tag a release (git tag v0.1.0 && git push --tags) after bumping the version in
|
|
4
|
+
# pyproject.toml and server.json.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
pypi:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: pypi
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write # PyPI trusted publishing
|
|
15
|
+
contents: write # GitHub Release
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
19
|
+
- name: Check versions match the tag
|
|
20
|
+
run: |
|
|
21
|
+
TAG=${GITHUB_REF_NAME#v}
|
|
22
|
+
grep -q "^version = \"$TAG\"" pyproject.toml
|
|
23
|
+
test "$(jq -r .version server.json)" = "$TAG"
|
|
24
|
+
test "$(jq -r '.packages[0].version' server.json)" = "$TAG"
|
|
25
|
+
- run: uv sync --locked && uv run pytest -q
|
|
26
|
+
- run: uv build
|
|
27
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
28
|
+
- run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes --title "$GITHUB_REF_NAME"
|
|
29
|
+
env:
|
|
30
|
+
GH_TOKEN: ${{ github.token }}
|
|
31
|
+
|
|
32
|
+
mcp-registry:
|
|
33
|
+
needs: pypi
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
permissions:
|
|
36
|
+
id-token: write # MCP Registry login via GitHub OIDC
|
|
37
|
+
contents: read
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v7
|
|
40
|
+
- name: Wait for PyPI to serve the new version
|
|
41
|
+
run: |
|
|
42
|
+
for i in $(seq 1 30); do
|
|
43
|
+
curl -sf "https://pypi.org/pypi/milligold-mcp/${GITHUB_REF_NAME#v}/json" >/dev/null && exit 0
|
|
44
|
+
sleep 10
|
|
45
|
+
done
|
|
46
|
+
exit 1
|
|
47
|
+
- name: Install mcp-publisher
|
|
48
|
+
run: |
|
|
49
|
+
curl -L "https://github.com/modelcontextprotocol/registry/releases/download/v1.8.1/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
|
50
|
+
- run: ./mcp-publisher login github-oidc
|
|
51
|
+
- run: ./mcp-publisher publish
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sepehr Radmard
|
|
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,76 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: milligold-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial read-only MCP server for Milli.gold (milli.gold): live 18k gold price, price history, platform stats.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sepehr071/milligold-mcp
|
|
6
|
+
Project-URL: Issues, https://github.com/sepehr071/milligold-mcp/issues
|
|
7
|
+
Author-email: Sepehr Radmard <sepehrr80@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: gold,iran,mcp,milli-gold,model-context-protocol,prices
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: curl-cffi>=0.16.3
|
|
22
|
+
Requires-Dist: httpx-curl-cffi>=0.1.5
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: mcp<3,>=2.2
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
<!-- mcp-name: io.github.sepehr071/milligold-mcp -->
|
|
28
|
+
|
|
29
|
+
<div align="center">
|
|
30
|
+
|
|
31
|
+
<img src="https://raw.githubusercontent.com/sepehr071/milligold-mcp/main/.github/banner.png" alt="milligold-mcp: unofficial Milli.gold MCP server for Claude, Cursor and Copilot" width="100%">
|
|
32
|
+
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
# milligold-mcp: Milli.gold price MCP server for AI agents
|
|
36
|
+
|
|
37
|
+
An unofficial, read-only MCP server for [Milli.gold](https://milli.gold) (میلی), an Iranian online platform for buying and
|
|
38
|
+
selling 18k melted gold by the milligram: the live gold price, price history and platform statistics.
|
|
39
|
+
|
|
40
|
+
## For AI agents
|
|
41
|
+
|
|
42
|
+
- **What:** unofficial MCP server for the public price and statistics endpoints of milli.gold.
|
|
43
|
+
- **Install:** `uvx milligold-mcp` or `claude mcp add milligold -- uvx milligold-mcp`. Transport: stdio. Needs [uv](https://docs.astral.sh/uv/getting-started/installation/) (Python 3.10+).
|
|
44
|
+
- **Auth:** none. Optional `MILLIGOLD_MCP_PROXY` (HTTP proxy URL, or a comma-separated list); system proxy settings are ignored.
|
|
45
|
+
- **Access:** read-only, so no confirmation is needed before calling any tool. It cannot log in, trade or read a wallet.
|
|
46
|
+
- **Tools:** 3, all prefixed `mg_`. Typical sequence: `mg_get_price` -> `mg_price_history`.
|
|
47
|
+
- **Output:** compact JSON. Prices are quoted by the site in **Rial per milligram** of 18k gold; replies also carry Rial and Toman per gram.
|
|
48
|
+
- **Machine-readable:** [`server.json`](server.json).
|
|
49
|
+
|
|
50
|
+
## Tools
|
|
51
|
+
|
|
52
|
+
| Tool | Purpose |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `mg_get_price` | Current Milli.gold price of 18k melted gold: Rial per milligram, Rial per gram, Toman per gram |
|
|
55
|
+
| `mg_price_history` | Price series for a day, week or month (thinned to at most 50 points) with first, last, high, low and change % |
|
|
56
|
+
| `mg_platform_stats` | Headline numbers: users, trade count, gold traded and held (kg), burned gold, branches |
|
|
57
|
+
|
|
58
|
+
## Units
|
|
59
|
+
|
|
60
|
+
`price18` is Rial per **milligram** (1 gram = 1000 mg): 268,900 Rial/mg = 268,900,000 Rial/g = 26,890,000 Toman/g, which
|
|
61
|
+
matches "price of 1 gram of 18k gold" on the site. Times are Tehran time (UTC+3:30). The price is Milli.gold's own, with no tax or
|
|
62
|
+
making charge, so it differs slightly from market quotes elsewhere.
|
|
63
|
+
|
|
64
|
+
Only three public read endpoints exist (`milli-price/external`, `milli-price/widget/external`, `milli-overall-report`); everything else on the site needs a login.
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
uv sync
|
|
70
|
+
uv run ruff check . && uv run ruff format --check .
|
|
71
|
+
uv run python -m pytest -q # offline unit tests
|
|
72
|
+
uv run python -m pytest -m live -q # calls milli.gold
|
|
73
|
+
uv build
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Not affiliated with Milli.gold. Prices are informational, not financial advice. MIT license.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.sepehr071/milligold-mcp -->
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<img src="https://raw.githubusercontent.com/sepehr071/milligold-mcp/main/.github/banner.png" alt="milligold-mcp: unofficial Milli.gold MCP server for Claude, Cursor and Copilot" width="100%">
|
|
6
|
+
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
# milligold-mcp: Milli.gold price MCP server for AI agents
|
|
10
|
+
|
|
11
|
+
An unofficial, read-only MCP server for [Milli.gold](https://milli.gold) (میلی), an Iranian online platform for buying and
|
|
12
|
+
selling 18k melted gold by the milligram: the live gold price, price history and platform statistics.
|
|
13
|
+
|
|
14
|
+
## For AI agents
|
|
15
|
+
|
|
16
|
+
- **What:** unofficial MCP server for the public price and statistics endpoints of milli.gold.
|
|
17
|
+
- **Install:** `uvx milligold-mcp` or `claude mcp add milligold -- uvx milligold-mcp`. Transport: stdio. Needs [uv](https://docs.astral.sh/uv/getting-started/installation/) (Python 3.10+).
|
|
18
|
+
- **Auth:** none. Optional `MILLIGOLD_MCP_PROXY` (HTTP proxy URL, or a comma-separated list); system proxy settings are ignored.
|
|
19
|
+
- **Access:** read-only, so no confirmation is needed before calling any tool. It cannot log in, trade or read a wallet.
|
|
20
|
+
- **Tools:** 3, all prefixed `mg_`. Typical sequence: `mg_get_price` -> `mg_price_history`.
|
|
21
|
+
- **Output:** compact JSON. Prices are quoted by the site in **Rial per milligram** of 18k gold; replies also carry Rial and Toman per gram.
|
|
22
|
+
- **Machine-readable:** [`server.json`](server.json).
|
|
23
|
+
|
|
24
|
+
## Tools
|
|
25
|
+
|
|
26
|
+
| Tool | Purpose |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `mg_get_price` | Current Milli.gold price of 18k melted gold: Rial per milligram, Rial per gram, Toman per gram |
|
|
29
|
+
| `mg_price_history` | Price series for a day, week or month (thinned to at most 50 points) with first, last, high, low and change % |
|
|
30
|
+
| `mg_platform_stats` | Headline numbers: users, trade count, gold traded and held (kg), burned gold, branches |
|
|
31
|
+
|
|
32
|
+
## Units
|
|
33
|
+
|
|
34
|
+
`price18` is Rial per **milligram** (1 gram = 1000 mg): 268,900 Rial/mg = 268,900,000 Rial/g = 26,890,000 Toman/g, which
|
|
35
|
+
matches "price of 1 gram of 18k gold" on the site. Times are Tehran time (UTC+3:30). The price is Milli.gold's own, with no tax or
|
|
36
|
+
making charge, so it differs slightly from market quotes elsewhere.
|
|
37
|
+
|
|
38
|
+
Only three public read endpoints exist (`milli-price/external`, `milli-price/widget/external`, `milli-overall-report`); everything else on the site needs a login.
|
|
39
|
+
|
|
40
|
+
## Development
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv sync
|
|
44
|
+
uv run ruff check . && uv run ruff format --check .
|
|
45
|
+
uv run python -m pytest -q # offline unit tests
|
|
46
|
+
uv run python -m pytest -m live -q # calls milli.gold
|
|
47
|
+
uv build
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Not affiliated with Milli.gold. Prices are informational, not financial advice. MIT license.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "milligold-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Unofficial read-only MCP server for Milli.gold (milli.gold): live 18k gold price, price history, platform stats."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Sepehr Radmard", email = "sepehrr80@gmail.com" }]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
keywords = ["mcp", "model-context-protocol", "milli-gold", "iran", "gold", "prices"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"mcp>=2.2,<3",
|
|
24
|
+
"httpx>=0.27",
|
|
25
|
+
"httpx-curl-cffi>=0.1.5",
|
|
26
|
+
"curl-cffi>=0.16.3",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/sepehr071/milligold-mcp"
|
|
31
|
+
Issues = "https://github.com/sepehr071/milligold-mcp/issues"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
milligold-mcp = "milligold_mcp.server:main"
|
|
35
|
+
|
|
36
|
+
[dependency-groups]
|
|
37
|
+
dev = ["pytest>=8", "ruff>=0.6"]
|
|
38
|
+
|
|
39
|
+
[build-system]
|
|
40
|
+
requires = ["hatchling"]
|
|
41
|
+
build-backend = "hatchling.build"
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
addopts = "-m 'not live'"
|
|
46
|
+
markers = ["live: calls the real Milli.gold API (run with -m live)"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 120
|
|
50
|
+
target-version = "py310"
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
54
|
+
ignore = ["E501"] # long Field(description=...) strings read better on one line
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.sepehr071/milligold-mcp",
|
|
4
|
+
"title": "Milli.gold Prices",
|
|
5
|
+
"description": "Unofficial read-only Milli.gold: live 18k gold price per milligram, history and platform stats.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/sepehr071/milligold-mcp",
|
|
8
|
+
"source": "github"
|
|
9
|
+
},
|
|
10
|
+
"websiteUrl": "https://github.com/sepehr071/milligold-mcp",
|
|
11
|
+
"version": "0.1.0",
|
|
12
|
+
"packages": [
|
|
13
|
+
{
|
|
14
|
+
"registryType": "pypi",
|
|
15
|
+
"registryBaseUrl": "https://pypi.org",
|
|
16
|
+
"identifier": "milligold-mcp",
|
|
17
|
+
"version": "0.1.0",
|
|
18
|
+
"runtimeHint": "uvx",
|
|
19
|
+
"transport": {
|
|
20
|
+
"type": "stdio"
|
|
21
|
+
},
|
|
22
|
+
"environmentVariables": [
|
|
23
|
+
{
|
|
24
|
+
"name": "MILLIGOLD_MCP_PROXY",
|
|
25
|
+
"description": "Optional HTTP proxy URL for milli.gold requests; system proxy settings are ignored. Also accepts a comma-separated list: one is used until it gets blocked or fails to connect, then the next.",
|
|
26
|
+
"isRequired": false,
|
|
27
|
+
"isSecret": false
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Milli.gold tools: live 18k gold price, price history, platform statistics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta, timezone
|
|
6
|
+
from typing import Annotated, Any, Literal
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from .http import API, ApiError, fetch
|
|
11
|
+
from .registry import tool
|
|
12
|
+
|
|
13
|
+
TEHRAN = timezone(timedelta(hours=3, minutes=30)) # no DST since 2022
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _data(body: Any, what: str) -> dict[str, Any]:
|
|
17
|
+
data = body.get("data") if isinstance(body, dict) else None
|
|
18
|
+
if not isinstance(body, dict) or body.get("code") != 0 or not isinstance(data, dict):
|
|
19
|
+
raise ApiError(f"Milli.gold returned no {what}.")
|
|
20
|
+
return data
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _price(rial_per_mg: int) -> dict[str, Any]:
|
|
24
|
+
return {
|
|
25
|
+
"rial_per_mg": rial_per_mg,
|
|
26
|
+
"rial_per_gram": rial_per_mg * 1000,
|
|
27
|
+
"toman_per_gram": rial_per_mg * 100,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@tool("Milli.gold live gold price")
|
|
32
|
+
async def mg_get_price() -> dict[str, Any]:
|
|
33
|
+
"""Get the current Milli.gold price of 18k melted gold, in Rial per milligram and per gram.
|
|
34
|
+
|
|
35
|
+
Milli.gold sells gold by the milligram (1 gram = 1000 mg); the quote is the platform's own price
|
|
36
|
+
with no tax or making charge. `as_of` is Tehran time. For the trend over a day, week or month use
|
|
37
|
+
mg_price_history.
|
|
38
|
+
"""
|
|
39
|
+
d = _data(await fetch(f"{API}/milli-price/external"), "price")
|
|
40
|
+
if not isinstance(d.get("price18"), int | float):
|
|
41
|
+
raise ApiError("Milli.gold returned no price.")
|
|
42
|
+
return {"as_of": d.get("date"), **_price(d["price18"]), "karat": 18}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@tool("Milli.gold price history")
|
|
46
|
+
async def mg_price_history(
|
|
47
|
+
period: Annotated[
|
|
48
|
+
Literal["DAILY", "WEEKLY", "MONTHLY"],
|
|
49
|
+
Field(description="Window: DAILY = last ~24 h (15-min points), WEEKLY = last week, MONTHLY = last ~month."),
|
|
50
|
+
] = "DAILY",
|
|
51
|
+
points: Annotated[int, Field(description="Max points to return, evenly spaced (2-50).", ge=2, le=50)] = 24,
|
|
52
|
+
) -> dict[str, Any]:
|
|
53
|
+
"""Get the Milli.gold 18k gold price series for a day, week or month, with high, low and change.
|
|
54
|
+
|
|
55
|
+
Prices are Rial per milligram; `time` is Tehran time. Points are thinned evenly to `points`.
|
|
56
|
+
"""
|
|
57
|
+
prices = _data(await fetch(f"{API}/milli-price/widget/external"), "price history").get("prices")
|
|
58
|
+
series = prices.get(period) if isinstance(prices, dict) else None
|
|
59
|
+
if not series or not all(isinstance(p, dict) and p.get("timestamp") and p.get("value") for p in series):
|
|
60
|
+
raise ApiError(f"Milli.gold returned no {period} history.")
|
|
61
|
+
series = sorted(series, key=lambda p: p["timestamp"])
|
|
62
|
+
# Like the site: stats span every series from this window's start (the day's peak counts in the week) plus the live price.
|
|
63
|
+
every = [
|
|
64
|
+
p
|
|
65
|
+
for v in prices.values()
|
|
66
|
+
if isinstance(v, list)
|
|
67
|
+
for p in v
|
|
68
|
+
if isinstance(p, dict) and p.get("value") and p.get("timestamp", 0) >= series[0]["timestamp"]
|
|
69
|
+
]
|
|
70
|
+
values = [p["value"] for p in sorted(every, key=lambda p: p["timestamp"])]
|
|
71
|
+
try:
|
|
72
|
+
values.append(_data(await fetch(f"{API}/milli-price/external"), "price")["price18"])
|
|
73
|
+
except (ApiError, KeyError):
|
|
74
|
+
pass # live price is a refinement; the series alone is still valid
|
|
75
|
+
step = (len(series) - 1) / (points - 1)
|
|
76
|
+
picked = [series[round(i * step)] for i in range(min(points, len(series)))] if len(series) > points else series
|
|
77
|
+
first, last = values[0], values[-1]
|
|
78
|
+
return {
|
|
79
|
+
"period": period,
|
|
80
|
+
"unit": "rial_per_mg",
|
|
81
|
+
"first": first,
|
|
82
|
+
"last": last,
|
|
83
|
+
"high": max(values),
|
|
84
|
+
"low": min(values),
|
|
85
|
+
"change_percent": round((last - first) / first * 100, 2),
|
|
86
|
+
"points": [
|
|
87
|
+
{
|
|
88
|
+
"time": datetime.fromtimestamp(p["timestamp"] / 1000, TEHRAN).strftime("%Y-%m-%d %H:%M"),
|
|
89
|
+
"price": p["value"],
|
|
90
|
+
}
|
|
91
|
+
for p in picked
|
|
92
|
+
],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@tool("Milli.gold platform statistics")
|
|
97
|
+
async def mg_platform_stats() -> dict[str, Any]:
|
|
98
|
+
"""Get Milli.gold's headline numbers: users, trades, gold traded, gold held and branches.
|
|
99
|
+
|
|
100
|
+
Figures are rounded totals the company publishes; weights are in kilograms. For prices use mg_get_price.
|
|
101
|
+
"""
|
|
102
|
+
d = _data(await fetch(f"{API}/milli-overall-report"), "statistics")
|
|
103
|
+
return {
|
|
104
|
+
"users": d.get("overallNumberOfUsers"),
|
|
105
|
+
"trade_transactions": d.get("overallNumberOfTradeTransactions"),
|
|
106
|
+
"traded_gold_kg": d.get("overallTradedMilliTurnoverInKg"),
|
|
107
|
+
"user_balances_kg": d.get("overallUsersMilliBalanceInKg"),
|
|
108
|
+
"burned_kg": d.get("overallBurnedMilliAmountInKg"),
|
|
109
|
+
"branches_and_representatives": d.get("numberOfBranchesAndRepresentatives"),
|
|
110
|
+
}
|