adif-mcp 0.1.5__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.
- adif_mcp-0.1.5/.gitignore +90 -0
- adif_mcp-0.1.5/LICENSE +21 -0
- adif_mcp-0.1.5/PKG-INFO +93 -0
- adif_mcp-0.1.5/README.md +77 -0
- adif_mcp-0.1.5/mcp/manifest.json +6 -0
- adif_mcp-0.1.5/mcp/schemas/manifest.v1.json +46 -0
- adif_mcp-0.1.5/pyproject.toml +134 -0
- adif_mcp-0.1.5/scripts/build_hooks.py +66 -0
- adif_mcp-0.1.5/scripts/smoke.sh +71 -0
- adif_mcp-0.1.5/scripts/validate_manifest.py +63 -0
- adif_mcp-0.1.5/src/adif_mcp/__init__.py +123 -0
- adif_mcp-0.1.5/src/adif_mcp/cli.py +55 -0
- adif_mcp-0.1.5/src/adif_mcp/models.py +124 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
.venv/
|
|
2
|
+
|
|
3
|
+
.pytest_cache/
|
|
4
|
+
.cache/
|
|
5
|
+
data/*.duckdb
|
|
6
|
+
*.tar.gz
|
|
7
|
+
.DS_Store
|
|
8
|
+
.cache_history.json
|
|
9
|
+
z-my-notes.md
|
|
10
|
+
pre-release-checklist*
|
|
11
|
+
rubric*
|
|
12
|
+
todo*
|
|
13
|
+
src/adif_mcp/adif_meta.json
|
|
14
|
+
.smoke-venv/
|
|
15
|
+
dist/
|
|
16
|
+
build/
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.py[cod]
|
|
19
|
+
*$py.class
|
|
20
|
+
|
|
21
|
+
# ------------------------
|
|
22
|
+
# MkDocs Local Site Build
|
|
23
|
+
# ------------------------
|
|
24
|
+
site/
|
|
25
|
+
!mkdocs.yml
|
|
26
|
+
!docs/
|
|
27
|
+
!docs/**
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ------------------------
|
|
31
|
+
# OS cruft
|
|
32
|
+
# ------------------------
|
|
33
|
+
.DS_Store
|
|
34
|
+
Thumbs.db
|
|
35
|
+
|
|
36
|
+
# ------------------------
|
|
37
|
+
# Python / venv / tooling
|
|
38
|
+
# ------------------------
|
|
39
|
+
.venv/
|
|
40
|
+
|
|
41
|
+
.pytest_cache/
|
|
42
|
+
.coverage
|
|
43
|
+
htmlcov/
|
|
44
|
+
*.pyc
|
|
45
|
+
*.pyo
|
|
46
|
+
*.pyd
|
|
47
|
+
*.egg-info/
|
|
48
|
+
.eggs/
|
|
49
|
+
.cache/ # local caches (pytest, tools, etc.)
|
|
50
|
+
|
|
51
|
+
# ------------------------
|
|
52
|
+
# Streamlit & app secrets
|
|
53
|
+
# ------------------------
|
|
54
|
+
.streamlit/secrets.toml
|
|
55
|
+
.env
|
|
56
|
+
.env.* # .env.local, etc.
|
|
57
|
+
|
|
58
|
+
# ------------------------
|
|
59
|
+
# Data / artifacts
|
|
60
|
+
# ------------------------
|
|
61
|
+
data/*.duckdb # local DB produced by ingest
|
|
62
|
+
data/*.duckdb-wal
|
|
63
|
+
*.tar.gz # release/packaging artifacts
|
|
64
|
+
*.zip
|
|
65
|
+
|
|
66
|
+
# WSPR ingest cache tracking (local machine paths)
|
|
67
|
+
.cache_history.json
|
|
68
|
+
|
|
69
|
+
# Optional: any custom download cache dirs you use
|
|
70
|
+
.cache/
|
|
71
|
+
.cache_wspr/
|
|
72
|
+
|
|
73
|
+
# ------------------------
|
|
74
|
+
# Notebooks (if you add any later)
|
|
75
|
+
# ------------------------
|
|
76
|
+
.ipynb_checkpoints/
|
|
77
|
+
|
|
78
|
+
# ------------------------
|
|
79
|
+
# IDE/editor
|
|
80
|
+
# ------------------------
|
|
81
|
+
.vscode/
|
|
82
|
+
.idea/
|
|
83
|
+
*.code-workspace
|
|
84
|
+
|
|
85
|
+
# ------------------------
|
|
86
|
+
# Logs / temp
|
|
87
|
+
# ------------------------
|
|
88
|
+
*.log
|
|
89
|
+
tmp/
|
|
90
|
+
temp/
|
adif_mcp-0.1.5/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
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.
|
adif_mcp-0.1.5/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: adif-mcp
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: ADIF MCP core engine with plugin support (LoTW, eQSL)
|
|
5
|
+
Project-URL: ADIF Spec, https://adif.org.uk/315/ADIF_315.htm
|
|
6
|
+
Project-URL: Compatibility, https://adif-mcp.com/changelog/#adif-compatibility
|
|
7
|
+
Author: KI7MT
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Requires-Dist: click
|
|
12
|
+
Requires-Dist: pytest>=8.4.1
|
|
13
|
+
Requires-Dist: requests>=2.32.5
|
|
14
|
+
Requires-Dist: typing-extensions>=4.15.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# adif-mcp
|
|
18
|
+
|
|
19
|
+
Core [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) service for **Amateur Radio Logging**, per [ADIF 3.1.5 specification](https://adif.org.uk/315/ADIF_315.htm)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Resources
|
|
24
|
+
|
|
25
|
+
[](https://www.python.org/)
|
|
26
|
+
[](LICENSE)
|
|
27
|
+
|
|
28
|
+
## Core Project Health
|
|
29
|
+

|
|
30
|
+
[](https://github.com/KI7MT/adif-mcp/releases)
|
|
31
|
+
[](https://github.com/KI7MT/adif-mcp/tags)
|
|
32
|
+
[](https://github.com/KI7MT/adif-mcp/actions/workflows/ci.yml)
|
|
33
|
+
[](https://adif-mcp.com/)
|
|
34
|
+
[](https://github.com/KI7MT/wspr-ai-lite/actions/workflows/pre-commit.yml)
|
|
35
|
+
[](https://conventionalcommits.org)
|
|
36
|
+
[](https://github.com/KI7MT/adif-mcp/actions/workflows/ssl-expiry.yml)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## MCP / API Readiness
|
|
40
|
+
[](https://modelcontextprotocol.io/)
|
|
41
|
+
[](https://adif-mcp.com/mcp/manifest.html)
|
|
42
|
+
[](#)
|
|
43
|
+
[](https://github.com/KI7MT/adif-mcp/actions/workflows/manifest-validate.yml)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
## Compliance and Program Registry
|
|
47
|
+
[](#-compliance--provenance)
|
|
48
|
+
[](https://adif.org.uk/programids.html)
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Overview
|
|
53
|
+
|
|
54
|
+
This package defines the ADIF MCP core engine, with plugins for:
|
|
55
|
+
- LoTW (`adif-mcp-lotw`)
|
|
56
|
+
- eQSL (`adif-mcp-eqsl`)
|
|
57
|
+
|
|
58
|
+
Performs these tasks
|
|
59
|
+
- Validation & normalization of ADIF records
|
|
60
|
+
- Unified schema for consistent QSO storage and exchange
|
|
61
|
+
- MCP-ready tools for safe AI-agent access
|
|
62
|
+
- Foundation for service adapters (e.g., LoTW, eQSL)
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Why?
|
|
67
|
+
Every amateur radio logger supports ADIF, but implementations are fragmented.
|
|
68
|
+
`adif-mcp` offers a **single, standards-based interface** to make QSO data portable, auditable, and agent-friendly.
|
|
69
|
+
|
|
70
|
+
## Next Steps
|
|
71
|
+
- Build `adif-mcp-lotw` and `adif-mcp-eqsl` adapters
|
|
72
|
+
- Expose MCP tools for validation, award tracking, and service sync
|
|
73
|
+
- Support cross-logger interoperability with AI-driven agents
|
|
74
|
+
|
|
75
|
+
## Compliance & Provenance
|
|
76
|
+
|
|
77
|
+
ADIF-MCP and its plugins follow the [ADIF Specification](https://adif.org.uk) (currently 3.1.5) and use **registered Program IDs** to identify all exports:
|
|
78
|
+
|
|
79
|
+
- `ADIF-MCP` — Core engine
|
|
80
|
+
- `ADIF-MCP-LOTW` — Plugin for ARRL Logbook of The World
|
|
81
|
+
- `ADIF-MCP-EQSL` — Plugin for eQSL.cc
|
|
82
|
+
|
|
83
|
+
To ensure transparency and auditability, the project also uses **APP_ fields** for provenance when augmenting records.
|
|
84
|
+
Examples include:
|
|
85
|
+
|
|
86
|
+
- `APP_ADIF-MCP_OP` → operation performed (`normalize`, `validate`, `merge`)
|
|
87
|
+
- `APP_ADIF-MCP-LOTW_ACTION` → LoTW plugin operation
|
|
88
|
+
- `APP_ADIF-MCP-EQSL_TIME` → timestamp of eQSL merge
|
|
89
|
+
|
|
90
|
+
See the [Program ID & APP_ Field Policy](docs/program-id-policy.md) for full details.
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
MIT — open and free for amateur radio use.
|
adif_mcp-0.1.5/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# adif-mcp
|
|
2
|
+
|
|
3
|
+
Core [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) service for **Amateur Radio Logging**, per [ADIF 3.1.5 specification](https://adif.org.uk/315/ADIF_315.htm)
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Resources
|
|
8
|
+
|
|
9
|
+
[](https://www.python.org/)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
|
|
12
|
+
## Core Project Health
|
|
13
|
+

|
|
14
|
+
[](https://github.com/KI7MT/adif-mcp/releases)
|
|
15
|
+
[](https://github.com/KI7MT/adif-mcp/tags)
|
|
16
|
+
[](https://github.com/KI7MT/adif-mcp/actions/workflows/ci.yml)
|
|
17
|
+
[](https://adif-mcp.com/)
|
|
18
|
+
[](https://github.com/KI7MT/wspr-ai-lite/actions/workflows/pre-commit.yml)
|
|
19
|
+
[](https://conventionalcommits.org)
|
|
20
|
+
[](https://github.com/KI7MT/adif-mcp/actions/workflows/ssl-expiry.yml)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## MCP / API Readiness
|
|
24
|
+
[](https://modelcontextprotocol.io/)
|
|
25
|
+
[](https://adif-mcp.com/mcp/manifest.html)
|
|
26
|
+
[](#)
|
|
27
|
+
[](https://github.com/KI7MT/adif-mcp/actions/workflows/manifest-validate.yml)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Compliance and Program Registry
|
|
31
|
+
[](#-compliance--provenance)
|
|
32
|
+
[](https://adif.org.uk/programids.html)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Overview
|
|
37
|
+
|
|
38
|
+
This package defines the ADIF MCP core engine, with plugins for:
|
|
39
|
+
- LoTW (`adif-mcp-lotw`)
|
|
40
|
+
- eQSL (`adif-mcp-eqsl`)
|
|
41
|
+
|
|
42
|
+
Performs these tasks
|
|
43
|
+
- Validation & normalization of ADIF records
|
|
44
|
+
- Unified schema for consistent QSO storage and exchange
|
|
45
|
+
- MCP-ready tools for safe AI-agent access
|
|
46
|
+
- Foundation for service adapters (e.g., LoTW, eQSL)
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Why?
|
|
51
|
+
Every amateur radio logger supports ADIF, but implementations are fragmented.
|
|
52
|
+
`adif-mcp` offers a **single, standards-based interface** to make QSO data portable, auditable, and agent-friendly.
|
|
53
|
+
|
|
54
|
+
## Next Steps
|
|
55
|
+
- Build `adif-mcp-lotw` and `adif-mcp-eqsl` adapters
|
|
56
|
+
- Expose MCP tools for validation, award tracking, and service sync
|
|
57
|
+
- Support cross-logger interoperability with AI-driven agents
|
|
58
|
+
|
|
59
|
+
## Compliance & Provenance
|
|
60
|
+
|
|
61
|
+
ADIF-MCP and its plugins follow the [ADIF Specification](https://adif.org.uk) (currently 3.1.5) and use **registered Program IDs** to identify all exports:
|
|
62
|
+
|
|
63
|
+
- `ADIF-MCP` — Core engine
|
|
64
|
+
- `ADIF-MCP-LOTW` — Plugin for ARRL Logbook of The World
|
|
65
|
+
- `ADIF-MCP-EQSL` — Plugin for eQSL.cc
|
|
66
|
+
|
|
67
|
+
To ensure transparency and auditability, the project also uses **APP_ fields** for provenance when augmenting records.
|
|
68
|
+
Examples include:
|
|
69
|
+
|
|
70
|
+
- `APP_ADIF-MCP_OP` → operation performed (`normalize`, `validate`, `merge`)
|
|
71
|
+
- `APP_ADIF-MCP-LOTW_ACTION` → LoTW plugin operation
|
|
72
|
+
- `APP_ADIF-MCP-EQSL_TIME` → timestamp of eQSL merge
|
|
73
|
+
|
|
74
|
+
See the [Program ID & APP_ Field Policy](docs/program-id-policy.md) for full details.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
MIT — open and free for amateur radio use.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "MCP Manifest (minimal v1 subset)",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["name", "version", "tools"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"name": { "type": "string", "minLength": 1 },
|
|
8
|
+
"version": { "type": "string", "minLength": 1 },
|
|
9
|
+
"description": { "type": "string" },
|
|
10
|
+
"tools": {
|
|
11
|
+
"type": "array",
|
|
12
|
+
"items": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"required": ["name", "description", "input_schema", "output_schema"],
|
|
15
|
+
"properties": {
|
|
16
|
+
"name": { "type": "string", "minLength": 1 },
|
|
17
|
+
"description": { "type": "string", "minLength": 1 },
|
|
18
|
+
"input_schema": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"required": ["type", "properties"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"type": { "const": "object" },
|
|
23
|
+
"properties": { "type": "object" },
|
|
24
|
+
"required": { "type": "array", "items": { "type": "string" } },
|
|
25
|
+
"additionalProperties": { "type": "boolean" }
|
|
26
|
+
},
|
|
27
|
+
"additionalProperties": true
|
|
28
|
+
},
|
|
29
|
+
"output_schema": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"required": ["type", "properties"],
|
|
32
|
+
"properties": {
|
|
33
|
+
"type": { "const": "object" },
|
|
34
|
+
"properties": { "type": "object" },
|
|
35
|
+
"required": { "type": "array", "items": { "type": "string" } },
|
|
36
|
+
"additionalProperties": { "type": "boolean" }
|
|
37
|
+
},
|
|
38
|
+
"additionalProperties": true
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"additionalProperties": true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"additionalProperties": true
|
|
46
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "adif-mcp"
|
|
3
|
+
version = "0.1.5"
|
|
4
|
+
description = "ADIF MCP core engine with plugin support (LoTW, eQSL)"
|
|
5
|
+
authors = [{ name = "KI7MT" }]
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"click",
|
|
11
|
+
"pytest>=8.4.1",
|
|
12
|
+
"requests>=2.32.5",
|
|
13
|
+
"typing-extensions>=4.15.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
"ADIF Spec" = "https://adif.org.uk/315/ADIF_315.htm"
|
|
18
|
+
"Compatibility" = "https://adif-mcp.com/changelog/#adif-compatibility"
|
|
19
|
+
|
|
20
|
+
[tool.adif]
|
|
21
|
+
spec_version = "3.1.5"
|
|
22
|
+
features = ["core QSO model", "band/mode/QSL_RCVD enums"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
adif-mcp = "adif_mcp.cli:cli"
|
|
26
|
+
|
|
27
|
+
[tool.uv]
|
|
28
|
+
dev-dependencies = [
|
|
29
|
+
"pytest",
|
|
30
|
+
"mypy",
|
|
31
|
+
"ruff",
|
|
32
|
+
"commitizen",
|
|
33
|
+
"pydantic<2",
|
|
34
|
+
"jsonschema>=4.25.1",
|
|
35
|
+
"interrogate>=1.7.0",
|
|
36
|
+
"types-click>=7.1.8",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 88
|
|
41
|
+
target-version = "py311"
|
|
42
|
+
extend-exclude = [
|
|
43
|
+
".venv",
|
|
44
|
+
".smoke-venv",
|
|
45
|
+
"dist",
|
|
46
|
+
"build",
|
|
47
|
+
".eggs",
|
|
48
|
+
".pytest_cache",
|
|
49
|
+
"site",
|
|
50
|
+
"docs/_site",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = ["E", "F", "I"]
|
|
55
|
+
ignore = []
|
|
56
|
+
fixable = ["I"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint.per-file-ignores]
|
|
59
|
+
"scripts/*.py" = ["T201", "E402"]
|
|
60
|
+
"samples/*.py" = ["E402"]
|
|
61
|
+
|
|
62
|
+
[tool.mypy]
|
|
63
|
+
python_version = "3.11"
|
|
64
|
+
plugins = ["pydantic.mypy"]
|
|
65
|
+
strict = true
|
|
66
|
+
disable_error_code = ["annotation-unchecked", "call-arg"] # keep yours
|
|
67
|
+
warn_unused_ignores = true
|
|
68
|
+
warn_redundant_casts = true
|
|
69
|
+
warn_return_any = true
|
|
70
|
+
no_implicit_optional = true
|
|
71
|
+
ignore_missing_imports = true
|
|
72
|
+
|
|
73
|
+
[tool.commitizen]
|
|
74
|
+
name = "cz_conventional_commits"
|
|
75
|
+
version_provider = "pep621"
|
|
76
|
+
tag_format = "v$version"
|
|
77
|
+
update_changelog_on_bump = true
|
|
78
|
+
|
|
79
|
+
[[tool.mypy.overrides]]
|
|
80
|
+
module = "adif_mcp.cli"
|
|
81
|
+
disable_error_code = ["misc"]
|
|
82
|
+
ignore_missing_imports = true
|
|
83
|
+
|
|
84
|
+
[tool.interrogate]
|
|
85
|
+
fail-under = 80
|
|
86
|
+
verbose = 1
|
|
87
|
+
quiet = false
|
|
88
|
+
color = true
|
|
89
|
+
# Count only public API; skip dunders/private
|
|
90
|
+
ignore-init-method = true
|
|
91
|
+
ignore-magic = true
|
|
92
|
+
ignore-module = false
|
|
93
|
+
ignore-private = true
|
|
94
|
+
ignore-nested-functions = true
|
|
95
|
+
# Optional: don’t require docstrings for properties
|
|
96
|
+
ignore-properties = true
|
|
97
|
+
# Only measure our packages
|
|
98
|
+
paths = ["src", "scripts", "test"]
|
|
99
|
+
exclude = [".venv", ".smoke-venv", "dist", "build", "site", "docs/_site"]
|
|
100
|
+
|
|
101
|
+
[build-system]
|
|
102
|
+
requires = ["hatchling>=1.21"]
|
|
103
|
+
build-backend = "hatchling.build"
|
|
104
|
+
|
|
105
|
+
[tool.hatch.build.hooks.custom]
|
|
106
|
+
path = "scripts/build_hooks.py"
|
|
107
|
+
|
|
108
|
+
[tool.hatch.build.targets.wheel]
|
|
109
|
+
packages = ["src/adif_mcp"]
|
|
110
|
+
|
|
111
|
+
[tool.hatch.build.targets.sdist]
|
|
112
|
+
include = [
|
|
113
|
+
"src/**",
|
|
114
|
+
"pyproject.toml",
|
|
115
|
+
"README.md",
|
|
116
|
+
"LICENSE*",
|
|
117
|
+
"mcp/**",
|
|
118
|
+
"scripts/**",
|
|
119
|
+
]
|
|
120
|
+
exclude = [
|
|
121
|
+
".venv/**",
|
|
122
|
+
".smoke-venv/**",
|
|
123
|
+
"dist/**",
|
|
124
|
+
"build/**",
|
|
125
|
+
".git/**",
|
|
126
|
+
".github/**",
|
|
127
|
+
"docs/**",
|
|
128
|
+
"site/**",
|
|
129
|
+
".ruff_cache/**",
|
|
130
|
+
".mypy_cache/**",
|
|
131
|
+
".pytest_cache/**",
|
|
132
|
+
"tests/**",
|
|
133
|
+
"test/**",
|
|
134
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom Hatchling build hook for adif-mcp.
|
|
3
|
+
|
|
4
|
+
This hook runs at build time and copies the `[tool.adif]` table from
|
|
5
|
+
`pyproject.toml` into a generated JSON file at
|
|
6
|
+
`src/adif_mcp/adif_meta.json`.
|
|
7
|
+
|
|
8
|
+
Why:
|
|
9
|
+
- Ensures the ADIF spec version and supported features are baked into
|
|
10
|
+
built wheels and sdists (so the runtime does not need to parse
|
|
11
|
+
`pyproject.toml`).
|
|
12
|
+
- Keeps `pyproject.toml` as the single source of truth for metadata.
|
|
13
|
+
- Provides a consistent fallback mechanism: editable installs read
|
|
14
|
+
directly from `pyproject.toml`, while published artifacts use the
|
|
15
|
+
generated JSON.
|
|
16
|
+
|
|
17
|
+
The generated file is ignored by version control (`.gitignore`) and
|
|
18
|
+
is safe to regenerate on every build.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import pathlib
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
import tomllib # Python 3.11+
|
|
31
|
+
except Exception: # pragma: no cover
|
|
32
|
+
tomllib = None # type: ignore[assignment]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BuildHook(BuildHookInterface):
|
|
36
|
+
"""
|
|
37
|
+
Copy [tool.adif] from pyproject.toml into src/adif_mcp/adif_meta.json
|
|
38
|
+
during build, so wheels contain the ADIF spec metadata.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def initialize(self, version: str, build_data: dict) -> None: # noqa: D401
|
|
42
|
+
"""Run at build start; write `adif_meta.json` into the package directory."""
|
|
43
|
+
root = pathlib.Path(self.root)
|
|
44
|
+
pyproject = root / "pyproject.toml"
|
|
45
|
+
|
|
46
|
+
meta = {"spec_version": "0", "features": []}
|
|
47
|
+
|
|
48
|
+
if tomllib and pyproject.is_file():
|
|
49
|
+
try:
|
|
50
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
51
|
+
adif = (data.get("tool") or {}).get("adif") or {}
|
|
52
|
+
spec = adif.get("spec_version")
|
|
53
|
+
feats = adif.get("features")
|
|
54
|
+
if isinstance(spec, str):
|
|
55
|
+
meta["spec_version"] = spec
|
|
56
|
+
if isinstance(feats, list):
|
|
57
|
+
meta["features"] = [str(x) for x in feats]
|
|
58
|
+
except Exception as e: # pragma: no cover
|
|
59
|
+
print(
|
|
60
|
+
f"[build-hook] Warning: failed reading [tool.adif]: {e}",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
out = root / "src" / "adif_mcp" / "adif_meta.json"
|
|
65
|
+
out.write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
|
|
66
|
+
print(f"[build-hook] wrote {out}")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# scripts/smoke.sh
|
|
3
|
+
# Full local smoke test for adif-mcp using uv + an isolated install check.
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
banner() { printf "\n\033[1;36m[smoke]\033[0m %s\n" "$*"; }
|
|
8
|
+
|
|
9
|
+
# Move to repo root (script is expected in ./scripts/)
|
|
10
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
11
|
+
cd "$ROOT_DIR"
|
|
12
|
+
|
|
13
|
+
banner "env info"
|
|
14
|
+
command -v uv >/dev/null || { echo "uv not found. Install with: pip install uv (or: brew install uv)"; exit 1; }
|
|
15
|
+
uv --version || true
|
|
16
|
+
uv run python -V || true
|
|
17
|
+
|
|
18
|
+
banner "sync deps (frozen if lock present)"
|
|
19
|
+
uv sync
|
|
20
|
+
|
|
21
|
+
banner "lint (ruff)"
|
|
22
|
+
uv run ruff check .
|
|
23
|
+
|
|
24
|
+
banner "type-check (mypy)"
|
|
25
|
+
uv run mypy src
|
|
26
|
+
|
|
27
|
+
banner "docstrings (interrogate)"
|
|
28
|
+
uv run interrogate -c pyproject.toml
|
|
29
|
+
|
|
30
|
+
banner "manifest validation"
|
|
31
|
+
uv run python scripts/validate_manifest.py mcp/manifest.json
|
|
32
|
+
|
|
33
|
+
banner "unit tests (pytest)"
|
|
34
|
+
uv run pytest -q
|
|
35
|
+
|
|
36
|
+
banner "build (wheel + sdist)"
|
|
37
|
+
rm -rf dist build *.egg-info
|
|
38
|
+
uv build
|
|
39
|
+
ls -lh dist
|
|
40
|
+
|
|
41
|
+
banner "install built wheel in a fresh, isolated venv"
|
|
42
|
+
VENV=".smoke-venv"
|
|
43
|
+
rm -rf "$VENV"
|
|
44
|
+
uv venv --seed "$VENV"
|
|
45
|
+
|
|
46
|
+
PY="$VENV/bin/python"
|
|
47
|
+
PIP="$VENV/bin/pip"
|
|
48
|
+
BIN="$VENV/bin"
|
|
49
|
+
|
|
50
|
+
echo "[smoke] python: $($PY -V)"
|
|
51
|
+
echo "[smoke] install wheel"
|
|
52
|
+
$PIP install --upgrade pip >/dev/null
|
|
53
|
+
$PIP install dist/*.whl
|
|
54
|
+
|
|
55
|
+
echo "[smoke] sanity: CLI reports version"
|
|
56
|
+
"$BIN/adif-mcp" version
|
|
57
|
+
|
|
58
|
+
echo "[smoke] import/package sanity"
|
|
59
|
+
"$PY" - <<'PY'
|
|
60
|
+
import adif_mcp as m
|
|
61
|
+
print("import OK:", getattr(m, "__version__", "?"), "ADIF", getattr(m, "__adif_spec__", "?"))
|
|
62
|
+
PY
|
|
63
|
+
|
|
64
|
+
echo "[smoke] entrypoints"
|
|
65
|
+
"$BIN/adif-mcp" --help >/dev/null
|
|
66
|
+
"$BIN/adif-mcp" manifest-validate >/dev/null
|
|
67
|
+
|
|
68
|
+
# Optional: clean up the venv created by this smoke run
|
|
69
|
+
rm -rf "$VENV"
|
|
70
|
+
|
|
71
|
+
banner "OK — full smoke test passed"
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""Validate MCP manifest files against a minimal v1 schema.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
uv run python scripts/validate_manifest.py mcp/manifest.json
|
|
7
|
+
uv run python scripts/validate_manifest.py
|
|
8
|
+
# defaults to mcp/manifest.json if present
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Sequence
|
|
17
|
+
|
|
18
|
+
from jsonschema import validate # type: ignore[import-untyped]
|
|
19
|
+
|
|
20
|
+
SCHEMA_PATH = Path("mcp/schemas/manifest.v1.json")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_json(path: Path) -> dict:
|
|
24
|
+
"""Read and parse a JSON file into a Python dict."""
|
|
25
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def validate_one(path: Path, schema_path: Path = SCHEMA_PATH) -> bool:
|
|
29
|
+
"""Validate a single manifest file against the schema and print a short OK line."""
|
|
30
|
+
manifest = load_json(path)
|
|
31
|
+
schema = load_json(schema_path)
|
|
32
|
+
validate(instance=manifest, schema=schema)
|
|
33
|
+
print(f"{path}: OK (jsonschema)")
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main(argv: Sequence[str]) -> int:
|
|
38
|
+
"""CLI entrypoint. Validate one or more manifest paths.
|
|
39
|
+
|
|
40
|
+
If no paths are provided, defaults to `mcp/manifest.json` if present.
|
|
41
|
+
"""
|
|
42
|
+
files = (
|
|
43
|
+
[Path(a) for a in argv]
|
|
44
|
+
if argv
|
|
45
|
+
else ([Path("mcp/manifest.json")] if Path("mcp/manifest.json").exists() else [])
|
|
46
|
+
)
|
|
47
|
+
if not files:
|
|
48
|
+
print("No manifest paths provided and default not found.", file=sys.stderr)
|
|
49
|
+
return 2
|
|
50
|
+
|
|
51
|
+
failures = 0
|
|
52
|
+
for p in files:
|
|
53
|
+
try:
|
|
54
|
+
validate_one(p)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
print(f"{p}: INVALID: {e}", file=sys.stderr)
|
|
57
|
+
failures += 1
|
|
58
|
+
|
|
59
|
+
return 0 if failures == 0 else 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""
|
|
2
|
+
adif_mcp: Core package for ADIF MCP — models, CLI, and manifest tooling.
|
|
3
|
+
|
|
4
|
+
This module exposes:
|
|
5
|
+
- __version__: the installed package version (from package metadata)
|
|
6
|
+
- __adif_spec__: ADIF spec version we target (from pyproject [tool.adif], if available)
|
|
7
|
+
- __adif_features__: short list of notable supported features
|
|
8
|
+
(from pyproject [tool.adif], if available)
|
|
9
|
+
|
|
10
|
+
Design:
|
|
11
|
+
- In dev/editable installs, we read pyproject.toml ([tool.adif]) directly.
|
|
12
|
+
- In built wheels (where pyproject.toml isn’t packaged),
|
|
13
|
+
we fall back to baked-in defaults.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
from importlib import resources as _res
|
|
18
|
+
from importlib.metadata import PackageNotFoundError
|
|
19
|
+
from importlib.metadata import version as _pkg_version
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Dict, List, Tuple
|
|
22
|
+
|
|
23
|
+
# -------------------------
|
|
24
|
+
# Package version (metadata)
|
|
25
|
+
# -------------------------
|
|
26
|
+
try:
|
|
27
|
+
__version__ = _pkg_version("adif-mcp")
|
|
28
|
+
except PackageNotFoundError:
|
|
29
|
+
# Editable or not yet built — keep a harmless placeholder
|
|
30
|
+
__version__ = "0.0.0"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ------------------------------------------
|
|
34
|
+
# Read [tool.adif] from pyproject.toml (dev)
|
|
35
|
+
# ------------------------------------------
|
|
36
|
+
def _find_pyproject(start: Path) -> Path | None:
|
|
37
|
+
"""
|
|
38
|
+
Walk upward from `start` to locate a pyproject.toml file.
|
|
39
|
+
Returns the first match or None.
|
|
40
|
+
"""
|
|
41
|
+
cur = start.resolve()
|
|
42
|
+
for _ in range(6): # walk up a handful of levels; adjust if needed
|
|
43
|
+
cand = cur / "pyproject.toml"
|
|
44
|
+
if cand.is_file():
|
|
45
|
+
return cand
|
|
46
|
+
if cur.parent == cur:
|
|
47
|
+
break
|
|
48
|
+
cur = cur.parent
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _load_adif_from_pyproject() -> Tuple[str, List[str]] | None:
|
|
53
|
+
"""
|
|
54
|
+
Try to load `[tool.adif]` from a nearby pyproject.toml.
|
|
55
|
+
Returns (spec_version, features) if found, else None.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
import tomllib # Python 3.11+
|
|
59
|
+
except Exception:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
here = Path(__file__).parent
|
|
63
|
+
pyproject = _find_pyproject(here)
|
|
64
|
+
if not pyproject:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
69
|
+
except Exception:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
tool = data.get("tool", {})
|
|
73
|
+
adif: Dict[str, Any] = tool.get("adif", {}) if isinstance(tool, dict) else {}
|
|
74
|
+
spec_version = adif.get("spec_version")
|
|
75
|
+
features = adif.get("features")
|
|
76
|
+
|
|
77
|
+
if isinstance(spec_version, str) and isinstance(features, list):
|
|
78
|
+
# Ensure features are strings
|
|
79
|
+
features_str = [str(x) for x in features]
|
|
80
|
+
return spec_version, features_str
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# --------------------------
|
|
85
|
+
# Fallback baked-in defaults
|
|
86
|
+
# --------------------------
|
|
87
|
+
|
|
88
|
+
# If pyproject.toml isn’t present at runtime (e.g., installed wheel),
|
|
89
|
+
# we still expose meaningful values. Keep these aligned with your releases.
|
|
90
|
+
|
|
91
|
+
_DEFAULT_ADIF_SPEC = "3.1.5"
|
|
92
|
+
_DEFAULT_ADIF_FEATURES = ["core QSO model", "band/mode/QSL_RCVD enums"]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _adif_meta() -> Tuple[str, List[str]]:
|
|
96
|
+
"""
|
|
97
|
+
Resolve ADIF metadata (spec version + features), preferring pyproject.toml.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
with (
|
|
102
|
+
_res.files("adif_mcp")
|
|
103
|
+
.joinpath("adif_meta.json")
|
|
104
|
+
.open("r", encoding="utf-8") as fh
|
|
105
|
+
):
|
|
106
|
+
data = json.load(fh)
|
|
107
|
+
spec = data.get("spec_version")
|
|
108
|
+
feats = data.get("features")
|
|
109
|
+
if isinstance(spec, str) and isinstance(feats, list):
|
|
110
|
+
return spec, [str(x) for x in feats]
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
loaded = _load_adif_from_pyproject()
|
|
115
|
+
if loaded:
|
|
116
|
+
return loaded
|
|
117
|
+
|
|
118
|
+
# Optional future enhancement:
|
|
119
|
+
# Try reading a packaged JSON resource like "adif_meta.json" here.
|
|
120
|
+
return _DEFAULT_ADIF_SPEC, _DEFAULT_ADIF_FEATURES
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
__adif_spec__, __adif_features__ = _adif_meta()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Command-line entry points for adif-mcp.
|
|
3
|
+
|
|
4
|
+
Commands:
|
|
5
|
+
- version -> prints package version + ADIF spec version
|
|
6
|
+
- manifest-validate -> quick shape/sanity validation for MCP manifest
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import pathlib
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
from adif_mcp import __adif_spec__, __version__
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.group()
|
|
20
|
+
@click.version_option(version=__version__, prog_name="adif-mcp")
|
|
21
|
+
def cli() -> None:
|
|
22
|
+
"""ADIF MCP core CLI."""
|
|
23
|
+
# No-op; subcommands below.
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@cli.command("version")
|
|
28
|
+
def version_cmd() -> None:
|
|
29
|
+
"""Show package version and ADIF spec compatibility."""
|
|
30
|
+
click.echo(f"adif-mcp {__version__} (ADIF {__adif_spec__} compatible)")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@cli.command("manifest-validate")
|
|
34
|
+
def manifest_validate() -> None:
|
|
35
|
+
"""
|
|
36
|
+
Validate the MCP manifest’s basic shape.
|
|
37
|
+
|
|
38
|
+
This is a lightweight check that ensures the file exists and has a top-level
|
|
39
|
+
'tools' array. For full schema validation, use the repo’s CI workflow or
|
|
40
|
+
the stricter validation script.
|
|
41
|
+
"""
|
|
42
|
+
p = pathlib.Path("mcp/manifest.json")
|
|
43
|
+
if not p.exists():
|
|
44
|
+
raise click.ClickException(f"manifest not found: {p}")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
data = json.loads(p.read_text(encoding="utf-8"))
|
|
48
|
+
except json.JSONDecodeError as e:
|
|
49
|
+
raise click.ClickException(f"invalid JSON in {p}: {e}") from e
|
|
50
|
+
|
|
51
|
+
tools = data.get("tools")
|
|
52
|
+
if not isinstance(tools, list):
|
|
53
|
+
raise click.ClickException("manifest.tools missing or not a list")
|
|
54
|
+
|
|
55
|
+
click.echo("manifest: OK")
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Pydantic models & constrained types for ADIF MCP core.
|
|
2
|
+
|
|
3
|
+
These models define a safe, minimal record shape used for validation and
|
|
4
|
+
normalization of ADIF-style QSO data, plus small batch I/O envelopes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Dict, List, Literal, Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
from typing_extensions import Annotated # <-- add this
|
|
13
|
+
|
|
14
|
+
# ---- Atomic field types (constrained) ----
|
|
15
|
+
Callsign = Annotated[str, Field(strip_whitespace=True, min_length=3, max_length=20)]
|
|
16
|
+
Band = Literal[
|
|
17
|
+
"160m",
|
|
18
|
+
"80m",
|
|
19
|
+
"60m",
|
|
20
|
+
"40m",
|
|
21
|
+
"30m",
|
|
22
|
+
"20m",
|
|
23
|
+
"17m",
|
|
24
|
+
"15m",
|
|
25
|
+
"12m",
|
|
26
|
+
"10m",
|
|
27
|
+
"6m",
|
|
28
|
+
"4m",
|
|
29
|
+
"2m",
|
|
30
|
+
"70cm",
|
|
31
|
+
"23cm",
|
|
32
|
+
]
|
|
33
|
+
Mode = Literal[
|
|
34
|
+
"CW",
|
|
35
|
+
"SSB",
|
|
36
|
+
"AM",
|
|
37
|
+
"FM",
|
|
38
|
+
"RTTY",
|
|
39
|
+
"PSK31",
|
|
40
|
+
"FT8",
|
|
41
|
+
"FT4",
|
|
42
|
+
"JT65",
|
|
43
|
+
"JT9",
|
|
44
|
+
"MFSK",
|
|
45
|
+
"OLIVIA",
|
|
46
|
+
"OTHER",
|
|
47
|
+
]
|
|
48
|
+
RST = Annotated[str, Field(regex=r"^\d{2,3}[A-Z]?$")]
|
|
49
|
+
BoolFlag = Literal["Y", "N"]
|
|
50
|
+
QSLRcvd = Literal["Y", "N", "R", "I", "V", "Q", "E"]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class QsoCore(BaseModel):
|
|
54
|
+
"""QSO Base Model that includes:
|
|
55
|
+
station_call `str`
|
|
56
|
+
qso_date `str`
|
|
57
|
+
time_on `str`
|
|
58
|
+
band `str`
|
|
59
|
+
mode `str`
|
|
60
|
+
freq `float`
|
|
61
|
+
rst_sent `int`
|
|
62
|
+
ret_rcvd `int`
|
|
63
|
+
my_gridsquare `str`
|
|
64
|
+
tx_power `float`
|
|
65
|
+
comment 1str`
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
station_call: Callsign
|
|
69
|
+
call: Callsign
|
|
70
|
+
qso_date: Annotated[str, Field(regex=r"^\d{8}$")] # YYYYMMDD
|
|
71
|
+
time_on: Annotated[str, Field(regex=r"^\d{4}(\d{2})?$")] # HHMM[SS]
|
|
72
|
+
band: Band
|
|
73
|
+
mode: Mode
|
|
74
|
+
freq: Optional[Annotated[float, Field(gt=0)]] = None # MHz
|
|
75
|
+
rst_sent: Optional[RST] = None
|
|
76
|
+
rst_rcvd: Optional[RST] = None
|
|
77
|
+
my_gridsquare: Optional[Annotated[str, Field(max_length=8)]] = None
|
|
78
|
+
gridsquare: Optional[Annotated[str, Field(max_length=8)]] = None
|
|
79
|
+
tx_pwr: Optional[Annotated[float, Field(ge=0)]] = None # watts
|
|
80
|
+
comment: Optional[Annotated[str, Field(max_length=200)]] = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class QslStatus(BaseModel):
|
|
84
|
+
"""QSO Status for lotw qsl received and the dates"""
|
|
85
|
+
|
|
86
|
+
lotw_qsl_rcvd: Optional[QSLRcvd] = None
|
|
87
|
+
eqsl_qsl_rcvd: Optional[QSLRcvd] = None
|
|
88
|
+
lotw_qsl_date: Optional[Annotated[str, Field(regex=r"^\d{8}$")]] = None
|
|
89
|
+
eqsl_qsl_date: Optional[Annotated[str, Field(regex=r"^\d{8}$")]] = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class QsoRecord(QsoCore, QslStatus):
|
|
93
|
+
"""Normalized QSO record that merges core ADIF fields with QSL status.
|
|
94
|
+
|
|
95
|
+
`adif_fields` preserves the original name→value map for round-tripping.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
adif_fields: Dict[str, str] = Field(
|
|
99
|
+
default_factory=dict,
|
|
100
|
+
description="Original ADIF name→value map (optional).",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ValidateRequest(BaseModel):
|
|
105
|
+
"""Batch validation request wrapper."""
|
|
106
|
+
|
|
107
|
+
records: List[Dict[str, str]] # raw ADIF field dicts (pre-model)
|
|
108
|
+
strict: bool = True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class ValidateResult(BaseModel):
|
|
112
|
+
"""Batch validation result wrapper."""
|
|
113
|
+
|
|
114
|
+
ok: bool
|
|
115
|
+
errors: List[Dict[str, str]] # {index, field, message}
|
|
116
|
+
normalized: Optional[List[QsoRecord]] = None # present if ok
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class EnumList(BaseModel):
|
|
120
|
+
"""Enumerations exposed by the MCP (for UI drop-downs / validation)."""
|
|
121
|
+
|
|
122
|
+
modes: List[str]
|
|
123
|
+
bands: List[str]
|
|
124
|
+
qsl_rcvd: List[str]
|