rrk-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.
@@ -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/rrk-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,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ .pytest_cache/
6
+ .ruff_cache/
rrk_mcp-0.1.0/LICENSE ADDED
@@ -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.
rrk_mcp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,103 @@
1
+ Metadata-Version: 2.5
2
+ Name: rrk-mcp
3
+ Version: 0.1.0
4
+ Summary: Unofficial read-only MCP server for the Official Gazette of Iran (rrk.ir, روزنامه رسمی): laws, regulations and court rulings.
5
+ Project-URL: Homepage, https://github.com/sepehr071/rrk-mcp
6
+ Project-URL: Issues, https://github.com/sepehr071/rrk-mcp/issues
7
+ Project-URL: Website, https://mcpfarsi.ir/
8
+ Author-email: Sepehr Radmard <sepehrr80@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: claude,iran-law,laws,legal,mcp,model-context-protocol,official-gazette,persian,regulations,rrk,rrk-ir
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Text Processing
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: curl-cffi>=0.16.3
24
+ Requires-Dist: httpx-curl-cffi>=0.1.5
25
+ Requires-Dist: httpx>=0.27
26
+ Requires-Dist: mcp<3,>=2.2
27
+ Description-Content-Type: text/markdown
28
+
29
+ <!-- mcp-name: io.github.sepehr071/rrk-mcp -->
30
+
31
+ <div align="center">
32
+
33
+ # rrk-mcp: Official Gazette of Iran (روزنامه رسمی) laws and rulings MCP server for AI agents
34
+
35
+ **rrk-mcp is an unofficial, read-only MCP server for the laws and regulations database of the Official Gazette of Iran (rrk.ir).**<br>
36
+ Its 5 tools list the newest laws, search them by title, full text, group, approver, special issue number or gazette date,<br>
37
+ read the full text of a law, court ruling or regulation, and list the licensed exchange offices. No API key or account is needed.
38
+
39
+ [![PyPI](https://img.shields.io/pypi/v/rrk-mcp?color=2563eb)](https://pypi.org/project/rrk-mcp/)
40
+ [![Python](https://img.shields.io/pypi/pyversions/rrk-mcp)](https://pypi.org/project/rrk-mcp/)
41
+ [![License: MIT](https://img.shields.io/badge/license-MIT-16a34a)](https://github.com/sepehr071/rrk-mcp/blob/main/LICENSE)
42
+
43
+ [For AI agents](#for-ai-agents) · [Quick start](#quick-start) · [Tools](#tools) · [Limits](#limits) · [فارسی](#فارسی)
44
+
45
+ </div>
46
+
47
+ ---
48
+
49
+ ## For AI agents
50
+
51
+ - **What:** unofficial MCP server for rrk.ir, the Official Gazette: laws (قوانین), regulations (آیین‌نامه، مصوبات), Court of Administrative Justice rulings (آرای دیوان عدالت اداری) and other texts published in the gazette since 1285 (2026 = 1405).
52
+ - **Install:** `uvx rrk-mcp` or `claude mcp add rrk -- uvx rrk-mcp`. Transport: stdio. Needs [uv](https://docs.astral.sh/uv/getting-started/installation/) (Python 3.10+).
53
+ - **Auth:** none. Optional `RRK_MCP_PROXY` (HTTP proxy URL); system proxy settings are ignored.
54
+ - **Access:** read-only; no confirmation is needed before calling any tool.
55
+ - **Tools:** 5, all prefixed `rk_`. Typical sequence: `rk_law_filters` (optional) -> `rk_search_laws` -> `rk_law_text`.
56
+ - **Use when:** the user asks what the gazette published lately, wants a law or ruling by topic or date, or needs the text of one.
57
+ - **Do not use when:** the user wants company registration notices (founding, capital change, board change, liquidation). rrk.ir does not offer those in an open search, so this server cannot look companies up.
58
+ - **Output:** compact JSON, Persian text. Dates are Jalali (`1405/08/09`).
59
+
60
+ ## Quick start
61
+
62
+ ```bash
63
+ claude mcp add rrk -- uvx rrk-mcp
64
+ ```
65
+
66
+ Any other client: run `uvx rrk-mcp` as a stdio MCP server (`{"command": "uvx", "args": ["rrk-mcp"]}`).
67
+
68
+ ## Tools
69
+
70
+ | Tool | What it does |
71
+ |---|---|
72
+ | `rk_latest_laws` | List the newest laws, rulings and regulations in the Official Gazette, newest first |
73
+ | `rk_search_laws` | Search laws by title words, full-text words, law group, approver, special issue number or Jalali gazette date range |
74
+ | `rk_law_filters` | List the law groups (categories) and approving authorities to filter by |
75
+ | `rk_law_text` | Read one law's approver, gazette number and date, and full text (long texts in parts) |
76
+ | `rk_exchange_offices` | List the licensed currency exchange offices (location, phone, address) by province and city |
77
+
78
+ All 5 tools are annotated `readOnlyHint: true`.
79
+
80
+ ## Limits
81
+
82
+ - Laws only. Company notices (آگهی‌های ثبتی) sit behind rrk.ir's paid data service and login, so they are left out.
83
+ - The portal is slow (a full-text search can take 10 seconds) and shares one session; the server keeps one session and reuses it.
84
+ - `rk_law_text` works with any code, but a code the session has not listed is located by paging the list (10-60 s, and codes below about 10,000 may not be found). Majlis debates (group 64) have a title only; the debate text is a PDF behind login.
85
+ - Gazette PDFs and the shop need login or payment, and are left out.
86
+ - Persian ی and ک are converted to the Arabic ي and ك the portal stores; you can type either.
87
+
88
+ ## Development
89
+
90
+ ```bash
91
+ uv sync
92
+ uv run ruff check . && uv run ruff format --check .
93
+ uv run python -m pytest -q
94
+ uv run python -m pytest -m live -q
95
+ ```
96
+
97
+ ## فارسی
98
+
99
+ `rrk-mcp` یک سرور MCP غیررسمی و فقط‌خواندنی برای پایگاه قوانین و مقررات روزنامه رسمی (rrk.ir) است: فهرست جدیدترین قوانین، جستجو بر اساس عنوان، متن، شماره ویژه‌نامه و تاریخ روزنامه، فیلتر بر اساس گروه قوانین و مرجع تصویب، خواندن متن کامل قانون یا رأی، و فهرست صرافی‌های مجاز بانک مرکزی. جستجوی آگهی‌های ثبتی شرکت‌ها در سایت به‌صورت باز وجود ندارد و پشتیبانی نمی‌شود.
100
+
101
+ ---
102
+
103
+ Made by Sepehr Radmard (سپهر رادمرد) · [github.com/sepehr071](https://github.com/sepehr071) · [linkedin.com/in/sepehr-radmard](https://linkedin.com/in/sepehr-radmard)
@@ -0,0 +1,75 @@
1
+ <!-- mcp-name: io.github.sepehr071/rrk-mcp -->
2
+
3
+ <div align="center">
4
+
5
+ # rrk-mcp: Official Gazette of Iran (روزنامه رسمی) laws and rulings MCP server for AI agents
6
+
7
+ **rrk-mcp is an unofficial, read-only MCP server for the laws and regulations database of the Official Gazette of Iran (rrk.ir).**<br>
8
+ Its 5 tools list the newest laws, search them by title, full text, group, approver, special issue number or gazette date,<br>
9
+ read the full text of a law, court ruling or regulation, and list the licensed exchange offices. No API key or account is needed.
10
+
11
+ [![PyPI](https://img.shields.io/pypi/v/rrk-mcp?color=2563eb)](https://pypi.org/project/rrk-mcp/)
12
+ [![Python](https://img.shields.io/pypi/pyversions/rrk-mcp)](https://pypi.org/project/rrk-mcp/)
13
+ [![License: MIT](https://img.shields.io/badge/license-MIT-16a34a)](https://github.com/sepehr071/rrk-mcp/blob/main/LICENSE)
14
+
15
+ [For AI agents](#for-ai-agents) · [Quick start](#quick-start) · [Tools](#tools) · [Limits](#limits) · [فارسی](#فارسی)
16
+
17
+ </div>
18
+
19
+ ---
20
+
21
+ ## For AI agents
22
+
23
+ - **What:** unofficial MCP server for rrk.ir, the Official Gazette: laws (قوانین), regulations (آیین‌نامه، مصوبات), Court of Administrative Justice rulings (آرای دیوان عدالت اداری) and other texts published in the gazette since 1285 (2026 = 1405).
24
+ - **Install:** `uvx rrk-mcp` or `claude mcp add rrk -- uvx rrk-mcp`. Transport: stdio. Needs [uv](https://docs.astral.sh/uv/getting-started/installation/) (Python 3.10+).
25
+ - **Auth:** none. Optional `RRK_MCP_PROXY` (HTTP proxy URL); system proxy settings are ignored.
26
+ - **Access:** read-only; no confirmation is needed before calling any tool.
27
+ - **Tools:** 5, all prefixed `rk_`. Typical sequence: `rk_law_filters` (optional) -> `rk_search_laws` -> `rk_law_text`.
28
+ - **Use when:** the user asks what the gazette published lately, wants a law or ruling by topic or date, or needs the text of one.
29
+ - **Do not use when:** the user wants company registration notices (founding, capital change, board change, liquidation). rrk.ir does not offer those in an open search, so this server cannot look companies up.
30
+ - **Output:** compact JSON, Persian text. Dates are Jalali (`1405/08/09`).
31
+
32
+ ## Quick start
33
+
34
+ ```bash
35
+ claude mcp add rrk -- uvx rrk-mcp
36
+ ```
37
+
38
+ Any other client: run `uvx rrk-mcp` as a stdio MCP server (`{"command": "uvx", "args": ["rrk-mcp"]}`).
39
+
40
+ ## Tools
41
+
42
+ | Tool | What it does |
43
+ |---|---|
44
+ | `rk_latest_laws` | List the newest laws, rulings and regulations in the Official Gazette, newest first |
45
+ | `rk_search_laws` | Search laws by title words, full-text words, law group, approver, special issue number or Jalali gazette date range |
46
+ | `rk_law_filters` | List the law groups (categories) and approving authorities to filter by |
47
+ | `rk_law_text` | Read one law's approver, gazette number and date, and full text (long texts in parts) |
48
+ | `rk_exchange_offices` | List the licensed currency exchange offices (location, phone, address) by province and city |
49
+
50
+ All 5 tools are annotated `readOnlyHint: true`.
51
+
52
+ ## Limits
53
+
54
+ - Laws only. Company notices (آگهی‌های ثبتی) sit behind rrk.ir's paid data service and login, so they are left out.
55
+ - The portal is slow (a full-text search can take 10 seconds) and shares one session; the server keeps one session and reuses it.
56
+ - `rk_law_text` works with any code, but a code the session has not listed is located by paging the list (10-60 s, and codes below about 10,000 may not be found). Majlis debates (group 64) have a title only; the debate text is a PDF behind login.
57
+ - Gazette PDFs and the shop need login or payment, and are left out.
58
+ - Persian ی and ک are converted to the Arabic ي and ك the portal stores; you can type either.
59
+
60
+ ## Development
61
+
62
+ ```bash
63
+ uv sync
64
+ uv run ruff check . && uv run ruff format --check .
65
+ uv run python -m pytest -q
66
+ uv run python -m pytest -m live -q
67
+ ```
68
+
69
+ ## فارسی
70
+
71
+ `rrk-mcp` یک سرور MCP غیررسمی و فقط‌خواندنی برای پایگاه قوانین و مقررات روزنامه رسمی (rrk.ir) است: فهرست جدیدترین قوانین، جستجو بر اساس عنوان، متن، شماره ویژه‌نامه و تاریخ روزنامه، فیلتر بر اساس گروه قوانین و مرجع تصویب، خواندن متن کامل قانون یا رأی، و فهرست صرافی‌های مجاز بانک مرکزی. جستجوی آگهی‌های ثبتی شرکت‌ها در سایت به‌صورت باز وجود ندارد و پشتیبانی نمی‌شود.
72
+
73
+ ---
74
+
75
+ Made by Sepehr Radmard (سپهر رادمرد) · [github.com/sepehr071](https://github.com/sepehr071) · [linkedin.com/in/sepehr-radmard](https://linkedin.com/in/sepehr-radmard)
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "rrk-mcp"
3
+ version = "0.1.0"
4
+ description = "Unofficial read-only MCP server for the Official Gazette of Iran (rrk.ir, روزنامه رسمی): laws, regulations and court rulings."
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", "rrk", "rrk-ir", "official-gazette", "iran-law", "laws", "regulations", "legal", "claude", "persian"]
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
+ "Topic :: Text Processing",
22
+ ]
23
+ dependencies = [
24
+ "mcp>=2.2,<3",
25
+ "httpx>=0.27",
26
+ "httpx-curl-cffi>=0.1.5",
27
+ "curl-cffi>=0.16.3",
28
+ ]
29
+
30
+ [project.urls]
31
+ Homepage = "https://github.com/sepehr071/rrk-mcp"
32
+ Issues = "https://github.com/sepehr071/rrk-mcp/issues"
33
+ Website = "https://mcpfarsi.ir/"
34
+
35
+ [project.scripts]
36
+ rrk-mcp = "rrk_mcp.server:main"
37
+
38
+ [dependency-groups]
39
+ dev = ["pytest>=8", "ruff>=0.6"]
40
+
41
+ [build-system]
42
+ requires = ["hatchling"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [tool.pytest.ini_options]
46
+ testpaths = ["tests"]
47
+ addopts = "-m 'not live'"
48
+ markers = ["live: calls the real rrk.ir portal (run with -m live)"]
49
+
50
+ [tool.ruff]
51
+ line-length = 120
52
+ target-version = "py310"
53
+
54
+ [tool.ruff.lint]
55
+ select = ["E", "F", "I", "B", "UP"]
56
+ 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/rrk-mcp",
4
+ "title": "Official Gazette of Iran",
5
+ "description": "Read-only Official Gazette of Iran (rrk.ir): search laws and rulings, read their text.",
6
+ "repository": {
7
+ "url": "https://github.com/sepehr071/rrk-mcp",
8
+ "source": "github"
9
+ },
10
+ "websiteUrl": "https://github.com/sepehr071/rrk-mcp",
11
+ "version": "0.1.0",
12
+ "packages": [
13
+ {
14
+ "registryType": "pypi",
15
+ "registryBaseUrl": "https://pypi.org",
16
+ "identifier": "rrk-mcp",
17
+ "version": "0.1.0",
18
+ "runtimeHint": "uvx",
19
+ "transport": {
20
+ "type": "stdio"
21
+ },
22
+ "environmentVariables": [
23
+ {
24
+ "name": "RRK_MCP_PROXY",
25
+ "description": "Optional HTTP proxy URL for the rrk.ir requests; system proxy settings are ignored.",
26
+ "isRequired": false,
27
+ "isSecret": false
28
+ }
29
+ ]
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,5 @@
1
+ """Unofficial MCP server for the Official Gazette of Iran (rrk.ir): laws, regulations and rulings."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("rrk-mcp")
@@ -0,0 +1,38 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIGnTCCBIWgAwIBAgIRAKgt2eXcr98TIF5wBD5rlagwDQYJKoZIhvcNAQENBQAw
3
+ ejELMAkGA1UEBhMCUEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5B
4
+ LjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MR8wHQYD
5
+ VQQDExZDZXJ0dW0gVHJ1c3RlZCBSb290IENBMB4XDTI0MDYxODA3NDEyMloXDTM5
6
+ MDYwNTA3NDEyMlowUjELMAkGA1UEBhMCUEwxITAfBgNVBAoMGEFzc2VjbyBEYXRh
7
+ IFN5c3RlbXMgUy5BLjEgMB4GA1UEAwwXQ2VydHVtIERWIFRMUyBHMiBSMzkgQ0Ew
8
+ ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCo52NXEXWoO2w6zQeBtNer
9
+ d5ahhe8RgM8XVpwGEFoovjHc4K+Cp3auUWVlt7/ARthJoxOttF+jaSrlSve9mWnm
10
+ TJOo1QLuoOTWuZ9XUkMjDG1ztTbFsgRqQyOtZsDqniHD79wqD49DQW4geVslp9/L
11
+ iTQKUpPawtAwpBeoaRXL8RJ8xjNA+2bEr6vesz2MEvvhpWBSWNAIR5O5YbiLztQ9
12
+ KdOuBYS0CW59ptuCjg3AuLcp8aOjk9z/kJc8xKkO48hLTp+HpdHkuI+iFWZn0aCL
13
+ lM/ngpdoBw+NGs6TMC8B6BcK7y/zl8FsNC4gE86Kfd8J9zWhCA7umHnBXCSYCKRx
14
+ H5o7DtoGiWXvcRKYpGtWt9czdUa1edSk5mTrwZGEXLAkX1ECiAq4GS5vEGjrEQ1u
15
+ x8mag2LDh7ZnXdcyzkKZKGsx7uExe3Nx5gWWZMXFrZ5v+uxynKogHUY2vdIMB3dn
16
+ 9qRYwpzvn3msfBbkRTAcS9eis1AY0Xxqlt3aXkVyqfKhdJxOPpzATM+Ve4jZSd1n
17
+ LzEj+kFuHnv2jyOY3Vb35n3EmW8yAwG1OWX/QnemMA5s2fZ+ZydHOTG4DkwXnaTr
18
+ R/vUhM+FNywNUlvzYjcM6zt3Ysf9M1hK5PjUEKzsPf5BrIp0fs1zhlVC+cgBN2+J
19
+ PtYwxP1nNpxwBgtIPoTk6wIDAQABo4IBRDCCAUAwcQYIKwYBBQUHAQEEZTBjMDcG
20
+ CCsGAQUFBzAChitodHRwOi8vc3ViY2EucmVwb3NpdG9yeS5jZXJ0dW0ucGwvY3Ry
21
+ Y2EuY2VyMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0uY29t
22
+ MB8GA1UdIwQYMBaAFIz7HHW8AtOfTi5I2flgVKrEs0/6MBIGA1UdEwEB/wQIMAYB
23
+ Af8CAQAwNQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL3N1YmNhLmNybC5jZXJ0dW0u
24
+ cGwvY3RyY2EuY3JsMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAOBgNV
25
+ HQ8BAf8EBAMCAQYwEQYDVR0gBAowCDAGBgRVHSAAMB0GA1UdDgQWBBQzqHe3AThv
26
+ Xx8kJKIebBTjbiID3zANBgkqhkiG9w0BAQ0FAAOCAgEACDQ1ggBelZZ/NYs1nFKW
27
+ HnDrA8Y4pv0lvxLzSAC4ejGavMXqPTXHA+DEh9kHNd8tVlo24+6YN96Gspb1kMXR
28
+ uuql23/6R6Fpqg49dkQ1/DobsWvAHoYeZvsaAgaKRD3bvsAcB0JBhyBVT/88S9gu
29
+ DnS5YKMldiLMkVW1Noskd4dHEJ2mkJcVzJIJ0Y4johA1lC1JnZMjkB8ZTNIblkgJ
30
+ K6PqlhYkeMOkx+XbmUuUgh29T0sPne7/V6PHnbEJIxUs40+iLCF0HrdqZypjvWQq
31
+ pSmHRHI3UWVERDeERca0uJ3I+a5ER9vUL9u5ilGG4afyx7QwzitBG+1rU3nRsHyZ
32
+ g6osILL/MWc0AbWJMyKzQ9Guj+uwq47h6BC9BsWF34pJeDC8EuN3HNxPlSWSII9l
33
+ Omwtipvq0EL1iocJhXdlsG+jIUVs/Sl/Um9JiZV+h/MoytnrPrWMIj+0zz6BdaPP
34
+ 2sT6wcLzpnwYcE9FWSbQrzNpL283EOUkObjc8AIxICzPHGusF0IqsO+sj9XzvLTh
35
+ TjKfFlzx4NR8gbK7m8sXq6cgP4UAtyvDswebFIRQiuhjqOT9G7+56+4zC0RaEZx/
36
+ LwoFE+ObVXxX674szQvIc+7WPCooVsUbwZIikzJqZb4gJQ1OQx23CgyyYlsPHIDN
37
+ 8FpPkganuCwy++7umTkM7+Q=
38
+ -----END CERTIFICATE-----
@@ -0,0 +1,139 @@
1
+ """Tool for the list of licensed currency exchange offices that the Official Gazette publishes (rrk.ir, page 176).
2
+
3
+ Same APEX mechanics as laws.py: load the page once for a session, then post the classic report's refresh call.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import html
10
+ import json
11
+ import re
12
+ from typing import Annotated, Any
13
+
14
+ from pydantic import Field
15
+
16
+ from . import http
17
+ from .http import ApiError
18
+ from .laws import AJAX_URL, _field, _strip
19
+ from .registry import tool
20
+
21
+ PAGE_URL = f"{http.SITE}/ords/r/rrs/rrs-front/r-exchanges"
22
+ PER_PAGE = 50 # the report's fixed page size
23
+ COLUMNS = ("STATENAME", "CITYNAME", "REGION", "PHONENUMBER", "ZIPCODE", "ADDRESS")
24
+
25
+ _state: dict[str, Any] = {}
26
+ _lock: asyncio.Lock | None = None
27
+
28
+
29
+ class _SessionLost(ApiError):
30
+ """The portal answered with something other than the report (expired session); retried once with a new one."""
31
+
32
+
33
+ def reset() -> None:
34
+ _state.clear()
35
+
36
+
37
+ async def _session(force: bool = False) -> dict[str, Any]:
38
+ global _lock
39
+ _lock = _lock or asyncio.Lock()
40
+ async with _lock:
41
+ if force or not _state:
42
+ h = await http.fetch(PAGE_URL, cache=False)
43
+ sel = re.search(r'<select[^>]*id="P176_PROVINCE".*?</select>', h, re.S)
44
+ _state.clear()
45
+ _state.update(
46
+ instance=_field(h, r'name="p_instance" value="(\d+)"'),
47
+ salt=_field(h, r'value="(\d+)" id="pSalt"'),
48
+ protected=_field(h, r'id="pPageItemsProtected" value="([^"]+)"'),
49
+ region=_field(h, r'apex\.widget\.report\.init\("R(\d+)"'),
50
+ request=_field(h, r'apex\.widget\.report\.init\("R\d+","([^"]+)"').replace("\\u002F", "/"),
51
+ provinces=[html.unescape(v) for v in re.findall(r'<option value="([^"]+)"', sel[0])] if sel else [],
52
+ )
53
+ return dict(_state)
54
+
55
+
56
+ def _parse(out: str, page: int) -> dict[str, Any]:
57
+ if "t-Report" not in out:
58
+ raise _SessionLost("rrk.ir returned no exchange list (session problem). Try again in a minute.")
59
+ m = re.search(r"t-Report-paginationText\">[^<]*?(\d+)\s*-\s*(\d+)\s*\S+\s*([\d,]+)", out)
60
+ total = int(m[3].replace(",", "")) if m else 0
61
+ offices = []
62
+ for row in re.findall(r"<tr>(<td class=\"t-Report-cell\".*?)</tr>", out, re.S):
63
+ cells = dict(re.findall(r'headers="([A-Z]+)"[^>]*>(.*?)</td>', row, re.S))
64
+ offices.append(
65
+ {
66
+ "province": _strip(cells.get("STATENAME", "")),
67
+ "city": _strip(cells.get("CITYNAME", "")),
68
+ "district": _strip(cells.get("REGION", "")),
69
+ "phone": _strip(cells.get("PHONENUMBER", "")),
70
+ "postal_code": _strip(cells.get("ZIPCODE", "")),
71
+ "address": _strip(cells.get("ADDRESS", "")),
72
+ }
73
+ )
74
+ return {"total": total, "page": page, "more": page * PER_PAGE < total, "offices": offices}
75
+
76
+
77
+ _PERSIAN = str.maketrans({"ي": "ی", "ك": "ک", "ى": "ی"}) # the province/city lists use Persian ی and ک
78
+
79
+
80
+ @tool("Licensed exchange offices")
81
+ async def rk_exchange_offices(
82
+ province: Annotated[str, Field(description="Province name as on the site, e.g. 'تهران', 'قم'. Empty = all.")] = "",
83
+ city: Annotated[
84
+ str, Field(description="City name as on the site, e.g. 'قم', 'سلفچگان'. Empty = whole province.")
85
+ ] = "",
86
+ page: Annotated[int, Field(description="Page number, 50 offices per page.", ge=1, le=200)] = 1,
87
+ ) -> dict[str, Any]:
88
+ """List the licensed currency exchange offices (صرافی مجاز) the Central Bank published in the Official Gazette (rrk.ir).
89
+
90
+ Use to see where licensed exchange offices are or to find one's address and phone. Filter by `province` and
91
+ `city` (Persian names; an unknown province is an error that lists the valid ones). Returns `total`, `more`
92
+ and up to 50 `offices` with province, city, district, phone, postal_code and address. Office names are not
93
+ published, only the location.
94
+ """
95
+ province, city = province.translate(_PERSIAN).strip(), city.translate(_PERSIAN).strip()
96
+ try:
97
+ return await _list(province, city, page, False)
98
+ except _SessionLost:
99
+ return await _list(province, city, page, True)
100
+
101
+
102
+ async def _list(province: str, city: str, page: int, force: bool) -> dict[str, Any]:
103
+ s = await _session(force)
104
+ if province and province not in s["provinces"]:
105
+ raise ApiError(f"Unknown province {province!r}. Valid: " + ", ".join(s["provinces"]))
106
+ if city and not province:
107
+ raise ApiError("Give the province together with the city.")
108
+ form = {
109
+ "p_flow_id": "200",
110
+ "p_flow_step_id": "176",
111
+ "p_instance": s["instance"],
112
+ "p_debug": "",
113
+ "p_request": "PLUGIN=" + s["request"],
114
+ "x01": s["region"],
115
+ "p_widget_action": "reset",
116
+ "p_json": json.dumps(
117
+ {
118
+ "pageItems": {
119
+ "itemsToSubmit": [{"n": "P176_PROVINCE", "v": province}, {"n": "P176_CITY", "v": city}],
120
+ "protected": s["protected"],
121
+ "rowVersion": "",
122
+ "formRegionChecksums": [],
123
+ },
124
+ "salt": s["salt"],
125
+ }
126
+ ),
127
+ }
128
+ if page > 1:
129
+ form |= {
130
+ "p_widget_action": "paginate",
131
+ "p_pg_min_row": str((page - 1) * PER_PAGE + 1),
132
+ "p_pg_max_rows": str(PER_PAGE),
133
+ "p_pg_rows_fetched": str(PER_PAGE),
134
+ }
135
+ out = await http.fetch(f"{AJAX_URL}?p_context=rrs-front/r-exchanges/{s['instance']}", data=form)
136
+ r = _parse(out, page)
137
+ if city and not r["total"]:
138
+ r["note"] = f"No office in city {city!r}; use the city name exactly as the site writes it."
139
+ return r