mirror of
https://github.com/hemna/aprsd-twitter-plugin.git
synced 2026-10-08 08:00:29 -04:00
chore: remove legacy setup.cfg
All build metadata is already fully covered in pyproject.toml. setup.cfg was a leftover from the pbr era with duplicate entries for package name, author, classifiers, entry points, and long description — all of which are present in pyproject.toml. Closes #1
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# APRSD Cursor Rules
|
||||
|
||||
This directory contains Cursor AI rules that enforce consistent coding patterns across all APRSD plugins and extensions.
|
||||
|
||||
## Available Rules
|
||||
|
||||
| Rule File | Description | Scope |
|
||||
|-----------|-------------|-------|
|
||||
| `aprsd-project-structure.mdc` | Standard project structure and organization | Always applies |
|
||||
| `aprsd-plugin-patterns.mdc` | Plugin development patterns and best practices | Applies to `*_plugin.py` files |
|
||||
| `aprsd-extension-patterns.mdc` | Extension development patterns | Applies to `*extension.py` files |
|
||||
| `aprsd-config-patterns.mdc` | Oslo.config configuration patterns | Applies to `conf/*.py` files |
|
||||
| `aprsd-pyproject-standards.mdc` | pyproject.toml standards and conventions | Applies to `pyproject.toml` files |
|
||||
| `aprsd-cli-patterns.mdc` | CLI command patterns | Applies to `cli.py` files |
|
||||
| `aprsd-testing-standards.mdc` | Testing standards and best practices | Applies to `test_*.py` files |
|
||||
|
||||
## How These Rules Work
|
||||
|
||||
- **Always Apply Rules**: `aprsd-project-structure.mdc` applies to every Cursor session in this project
|
||||
- **File-Specific Rules**: Other rules activate when you're working with matching files (e.g., plugin rules when editing a `*_plugin.py` file)
|
||||
|
||||
## Deploying to Plugin/Extension Projects
|
||||
|
||||
To apply these rules across all your APRSD plugin and extension projects, copy this entire `.cursor/rules/` directory to each project:
|
||||
|
||||
```bash
|
||||
# From the aprsd directory
|
||||
for dir in ../aprsd-plugins/*/; do
|
||||
mkdir -p "$dir/.cursor"
|
||||
cp -r .cursor/rules "$dir/.cursor/"
|
||||
done
|
||||
```
|
||||
|
||||
Or copy manually to specific projects:
|
||||
|
||||
```bash
|
||||
cp -r /path/to/aprsd/.cursor/rules /path/to/aprsd-joke-plugin/.cursor/
|
||||
cp -r /path/to/aprsd/.cursor/rules /path/to/aprsd-admin-extension/.cursor/
|
||||
# etc.
|
||||
```
|
||||
|
||||
## Customizing Rules
|
||||
|
||||
If a specific plugin/extension needs custom rules:
|
||||
|
||||
1. Copy these base rules to the project
|
||||
2. Add project-specific rules as additional `.mdc` files
|
||||
3. Modify existing rules if needed (but try to keep consistency)
|
||||
|
||||
## Rule Syntax
|
||||
|
||||
Each rule file uses this format:
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Brief description of the rule
|
||||
globs: **/*.py # File pattern (optional)
|
||||
alwaysApply: false # Set to true for universal rules
|
||||
---
|
||||
|
||||
# Rule Title
|
||||
|
||||
Rule content in markdown...
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
These rules help:
|
||||
|
||||
- Maintain consistent code patterns across all APRSD projects
|
||||
- Provide context-aware guidance when developing plugins/extensions
|
||||
- Enforce best practices automatically
|
||||
- Reduce code review feedback on structural issues
|
||||
- Onboard new contributors faster
|
||||
|
||||
## Updating Rules
|
||||
|
||||
When updating these rules:
|
||||
|
||||
1. Update in the main APRSD repository first
|
||||
2. Test the changes
|
||||
3. Deploy to plugin/extension projects
|
||||
4. Communicate changes to the team
|
||||
|
||||
## See Also
|
||||
|
||||
- [Cursor Rules Documentation](https://docs.cursor.com/context/rules-for-ai)
|
||||
- Main APRSD workspace rules in `/Users/I530566/devel/mine/hamradio/aprsd/.cursor/rules/`
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
description: CLI patterns for APRSD plugins and extensions
|
||||
globs: **/cli.py
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# APRSD CLI Patterns
|
||||
|
||||
## Config Export Command
|
||||
|
||||
All plugins/extensions must provide a config export CLI command in `cli.py`:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CLI tool for module-name configuration export.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
def export_config_cmd(format="json"):
|
||||
"""Export plugin configuration options."""
|
||||
try:
|
||||
from module_name.conf.opts import export_config
|
||||
|
||||
result = export_config(format=format)
|
||||
|
||||
if format == "json":
|
||||
print(result)
|
||||
else:
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
return 0
|
||||
except ImportError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
print("\nTo export config, install oslo.config:", file=sys.stderr)
|
||||
print(" pip install oslo.config", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error exporting config: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for CLI."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Export module-name configuration options"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=["dict", "json"],
|
||||
default="json",
|
||||
help="Output format (default: json)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
sys.exit(export_config_cmd(format=args.format))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
## CLI Best Practices
|
||||
|
||||
- Use argparse for command-line parsing
|
||||
- Provide clear help messages
|
||||
- Return appropriate exit codes (0 for success, non-zero for errors)
|
||||
- Write errors to stderr using `file=sys.stderr`
|
||||
- Handle ImportError gracefully with helpful messages
|
||||
- Include shebang `#!/usr/bin/env python3` at the top
|
||||
|
||||
## Additional CLI Commands
|
||||
|
||||
For extensions with additional commands, organize them in `cmds/` directory:
|
||||
|
||||
```
|
||||
module_name/
|
||||
├── cli.py (config export)
|
||||
└── cmds/
|
||||
├── __init__.py
|
||||
└── mycommand.py
|
||||
```
|
||||
|
||||
Each command should:
|
||||
|
||||
- Have a clear entry point function
|
||||
- Accept parsed arguments
|
||||
- Return proper exit codes
|
||||
- Log to appropriate streams
|
||||
- Handle errors gracefully
|
||||
|
||||
## Entry Point Registration
|
||||
|
||||
Register CLI commands in pyproject.toml:
|
||||
|
||||
```toml
|
||||
[project.scripts]
|
||||
"module-name-export-config" = "module_name.cli:main"
|
||||
"module-name-mycommand" = "module_name.cmds.mycommand:main"
|
||||
```
|
||||
|
||||
## Command Naming Convention
|
||||
|
||||
- Use hyphenated names for commands (e.g., `aprsd-joke-plugin-export-config`)
|
||||
- Start with the module name
|
||||
- End with the action (e.g., `export-config`)
|
||||
- Keep names descriptive but concise
|
||||
@@ -0,0 +1,196 @@
|
||||
---
|
||||
description: Oslo config patterns for APRSD plugins and extensions
|
||||
globs: **/conf/*.py
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# APRSD Configuration Patterns
|
||||
|
||||
## conf/opts.py Structure
|
||||
|
||||
Must include Apache license header and follow this standard pattern:
|
||||
|
||||
```python
|
||||
# Copyright 2015 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import importlib
|
||||
import importlib.util
|
||||
import os
|
||||
import pkgutil
|
||||
|
||||
LIST_OPTS_FUNC_NAME = "list_opts"
|
||||
|
||||
|
||||
def _tupleize(dct):
|
||||
"""Take the dict of options and convert to the 2-tuple format."""
|
||||
return [(key, val) for key, val in dct.items()]
|
||||
|
||||
|
||||
def list_opts():
|
||||
opts = collections.defaultdict(list)
|
||||
module_names = _list_module_names()
|
||||
imported_modules = _import_modules(module_names)
|
||||
_append_config_options(imported_modules, opts)
|
||||
return _tupleize(opts)
|
||||
|
||||
|
||||
def _list_module_names():
|
||||
module_names = []
|
||||
package_path = os.path.dirname(os.path.abspath(__file__))
|
||||
for _, modname, ispkg in pkgutil.iter_modules(path=[package_path]):
|
||||
if modname == "opts" or ispkg:
|
||||
continue
|
||||
else:
|
||||
module_names.append(modname)
|
||||
return module_names
|
||||
|
||||
|
||||
def _import_modules(module_names):
|
||||
imported_modules = []
|
||||
for modname in module_names:
|
||||
mod = importlib.import_module("module_name.conf." + modname)
|
||||
if not hasattr(mod, LIST_OPTS_FUNC_NAME):
|
||||
msg = (
|
||||
"The module 'module_name.conf.%s' should have a '%s' "
|
||||
"function which returns the config options."
|
||||
% (modname, LIST_OPTS_FUNC_NAME)
|
||||
)
|
||||
raise Exception(msg)
|
||||
else:
|
||||
imported_modules.append(mod)
|
||||
return imported_modules
|
||||
|
||||
|
||||
def _append_config_options(imported_modules, config_options):
|
||||
for mod in imported_modules:
|
||||
configs = mod.list_opts()
|
||||
for key, val in configs.items():
|
||||
config_options[key].extend(val)
|
||||
|
||||
|
||||
def export_config(format="dict"):
|
||||
"""Export configuration options as a simple data structure.
|
||||
|
||||
Returns dict or JSON string containing all configuration options.
|
||||
"""
|
||||
if importlib.util.find_spec("oslo_config") is None:
|
||||
raise ImportError(
|
||||
"oslo_config is required to export configuration. "
|
||||
"Install it with: pip install oslo.config"
|
||||
)
|
||||
|
||||
opts = list_opts()
|
||||
result = {}
|
||||
|
||||
for group_name, opt_list in opts:
|
||||
result[group_name] = []
|
||||
for opt in opt_list:
|
||||
opt_dict = {
|
||||
"name": opt.name,
|
||||
"type": type(opt).__name__,
|
||||
"default": getattr(opt, "default", None),
|
||||
"help": getattr(opt, "help", ""),
|
||||
"required": not hasattr(opt, "default")
|
||||
or getattr(opt, "default", None) is None,
|
||||
}
|
||||
|
||||
# Add additional attributes if available
|
||||
if hasattr(opt, "choices") and opt.choices:
|
||||
opt_dict["choices"] = list(opt.choices)
|
||||
if hasattr(opt, "secret") and opt.secret:
|
||||
opt_dict["secret"] = True
|
||||
if hasattr(opt, "min") and opt.min is not None:
|
||||
opt_dict["min"] = opt.min
|
||||
if hasattr(opt, "max") and opt.max is not None:
|
||||
opt_dict["max"] = opt.max
|
||||
|
||||
result[group_name].append(opt_dict)
|
||||
|
||||
if format == "json":
|
||||
import json
|
||||
return json.dumps(result, indent=2)
|
||||
return result
|
||||
```
|
||||
|
||||
## conf/main.py Pattern
|
||||
|
||||
Define plugin/extension-specific config options using oslo.config:
|
||||
|
||||
```python
|
||||
from oslo_config import cfg
|
||||
|
||||
# Define configuration group
|
||||
plugin_group = cfg.OptGroup(
|
||||
name="my_plugin",
|
||||
title="My Plugin Settings",
|
||||
)
|
||||
|
||||
# Define configuration options
|
||||
plugin_opts = [
|
||||
cfg.BoolOpt(
|
||||
"enabled",
|
||||
default=False,
|
||||
help="Enable the plugin",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"api_key",
|
||||
default=None,
|
||||
help="API key for external service",
|
||||
secret=True,
|
||||
),
|
||||
cfg.IntOpt(
|
||||
"timeout",
|
||||
default=30,
|
||||
help="Timeout in seconds for API calls",
|
||||
min=1,
|
||||
max=300,
|
||||
),
|
||||
]
|
||||
|
||||
def register_opts(config):
|
||||
"""Register configuration options."""
|
||||
config.register_group(plugin_group)
|
||||
config.register_opts(plugin_opts, group=plugin_group)
|
||||
|
||||
def list_opts():
|
||||
"""Return configuration options."""
|
||||
return {plugin_group.name: plugin_opts}
|
||||
```
|
||||
|
||||
## Configuration Best Practices
|
||||
|
||||
- Always include an `enabled` boolean option
|
||||
- Mark sensitive options with `secret=True`
|
||||
- Provide reasonable defaults
|
||||
- Include helpful documentation in help strings
|
||||
- Use appropriate option types (StrOpt, BoolOpt, IntOpt, ListOpt, etc.)
|
||||
- Set min/max bounds for numeric values
|
||||
- Use choices for enum-like options
|
||||
|
||||
## Accessing Configuration
|
||||
|
||||
In plugin/extension code:
|
||||
|
||||
```python
|
||||
from oslo_config import cfg
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
# Access config values
|
||||
if CONF.my_plugin.enabled:
|
||||
api_key = CONF.my_plugin.api_key
|
||||
```
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
description: APRSD extension development patterns
|
||||
globs: **/*extension.py
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# APRSD Extension Patterns
|
||||
|
||||
## Extension Base Class
|
||||
|
||||
Extensions provide additional functionality to APRSD beyond message processing plugins.
|
||||
|
||||
```python
|
||||
from aprsd import plugin
|
||||
|
||||
class MyExtension(plugin.APRSDExtensionBase):
|
||||
"""Extension description.
|
||||
|
||||
Extensions can add web interfaces, commands, background services, etc.
|
||||
"""
|
||||
|
||||
version = module_name.__version__
|
||||
```
|
||||
|
||||
## Common Extension Types
|
||||
|
||||
### Web Extensions
|
||||
|
||||
Extensions that add web interfaces (like aprsd-admin-extension, aprsd-webchat-extension):
|
||||
|
||||
```python
|
||||
def setup(self):
|
||||
"""Setup the extension."""
|
||||
self.enabled = CONF.my_extension.enabled
|
||||
|
||||
def create_threads(self):
|
||||
"""Create web server or other background threads."""
|
||||
if self.enabled:
|
||||
return [MyWebServerThread()]
|
||||
return []
|
||||
```
|
||||
|
||||
### CLI Extensions
|
||||
|
||||
Extensions that add CLI commands (like aprsd-trip-extension):
|
||||
|
||||
- Add commands in `cmds/` directory
|
||||
- Register entry points in pyproject.toml
|
||||
- Follow Click or argparse patterns consistently
|
||||
|
||||
### Background Service Extensions
|
||||
|
||||
Extensions that run background services:
|
||||
|
||||
- All threads must inherit from `APRSDThread` base class
|
||||
- Implement proper thread lifecycle methods
|
||||
- Handle graceful shutdown
|
||||
|
||||
## Configuration Pattern
|
||||
|
||||
Extensions should use oslo.config for configuration:
|
||||
|
||||
```python
|
||||
from oslo_config import cfg
|
||||
|
||||
extension_group = cfg.OptGroup(
|
||||
name="my_extension",
|
||||
title="My Extension Settings",
|
||||
)
|
||||
|
||||
extension_opts = [
|
||||
cfg.BoolOpt(
|
||||
"enabled",
|
||||
default=False,
|
||||
help="Enable the extension",
|
||||
),
|
||||
]
|
||||
```
|
||||
|
||||
## Entry Points
|
||||
|
||||
Extensions must define entry points in pyproject.toml:
|
||||
|
||||
```toml
|
||||
[project.entry-points."oslo.config.opts"]
|
||||
"module_name.conf" = "module_name.conf.opts:list_opts"
|
||||
```
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
description: APRSD plugin development patterns and best practices
|
||||
globs: **/*_plugin.py
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# APRSD Plugin Patterns
|
||||
|
||||
## Plugin Base Class
|
||||
|
||||
All plugins must inherit from `plugin.APRSDRegexCommandPluginBase`:
|
||||
|
||||
```python
|
||||
from aprsd import plugin, packets
|
||||
from aprsd.utils import trace
|
||||
import logging
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
class MyPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Plugin description goes here.
|
||||
|
||||
Explain what the plugin does, what commands it responds to,
|
||||
and any special configuration needed.
|
||||
"""
|
||||
|
||||
version = module_name.__version__
|
||||
command_regex = "^[cC]" # Regex to match command
|
||||
command_name = "mycommand" # One-word command name
|
||||
enabled = False
|
||||
```
|
||||
|
||||
## Required Methods
|
||||
|
||||
### setup()
|
||||
|
||||
Initialize plugin and set `self.enabled` flag:
|
||||
|
||||
```python
|
||||
def setup(self):
|
||||
"""Allows the plugin to do some 'setup' type checks.
|
||||
|
||||
If the setup checks fail, set self.enabled = False. This
|
||||
will prevent the plugin from being called when packets are received.
|
||||
"""
|
||||
self.enabled = CONF.my_plugin.enabled
|
||||
# Additional setup checks here
|
||||
```
|
||||
|
||||
### process()
|
||||
|
||||
Must use `@trace.trace` decorator and accept `Packet`:
|
||||
|
||||
```python
|
||||
@trace.trace
|
||||
def process(self, packet: packets.core.Packet):
|
||||
"""Process matching packet.
|
||||
|
||||
This is called when a received packet matches self.command_regex.
|
||||
Only called when self.enabled = True.
|
||||
"""
|
||||
if not self.enabled:
|
||||
LOG.info("Plugin is not enabled")
|
||||
return
|
||||
|
||||
message = packet.message_text
|
||||
|
||||
# Your logic here
|
||||
|
||||
return "response message"
|
||||
```
|
||||
|
||||
### help()
|
||||
|
||||
Return list of help strings:
|
||||
|
||||
```python
|
||||
def help(self):
|
||||
"""Return help message for the plugin."""
|
||||
return [
|
||||
f"{self.command_name}: Description of what command does",
|
||||
"Usage: command [options]",
|
||||
"Additional help lines if needed"
|
||||
]
|
||||
```
|
||||
|
||||
### create_threads()
|
||||
|
||||
Return list of APRSDThread objects (or empty list):
|
||||
|
||||
```python
|
||||
def create_threads(self):
|
||||
"""Create and return custom APRSDThread objects.
|
||||
|
||||
Create a child of aprsd.threads.APRSDThread object and return it.
|
||||
It will automatically get started.
|
||||
"""
|
||||
if self.enabled:
|
||||
# return [MyAPRSDThread()]
|
||||
return []
|
||||
```
|
||||
|
||||
## Message Formatting
|
||||
|
||||
- Wrap long messages using `textwrap.wrap(text, 67, break_long_words=False)`
|
||||
- APRS messages have a 67 character limit per line
|
||||
- Return strings or lists of strings from `process()`
|
||||
- Use `textwrap` module for proper text wrapping
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Always wrap external API calls in try/except blocks
|
||||
- Log errors with `LOG.error()`
|
||||
- Return user-friendly error messages
|
||||
- Don't let exceptions bubble up from `process()`
|
||||
|
||||
```python
|
||||
try:
|
||||
result = external_api_call()
|
||||
except Exception as e:
|
||||
LOG.error(f"Error calling API: {e}")
|
||||
return "Error: Unable to fetch data"
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
- Use `LOG = logging.getLogger("APRSD")` at module level
|
||||
- Use appropriate log levels: DEBUG, INFO, WARNING, ERROR
|
||||
- Include context in log messages
|
||||
@@ -0,0 +1,50 @@
|
||||
---
|
||||
description: Standard APRSD plugin/extension project structure
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# APRSD Project Structure Standards
|
||||
|
||||
All APRSD plugins and extensions must follow this structure:
|
||||
|
||||
```
|
||||
project-name/
|
||||
├── module_name/
|
||||
│ ├── __init__.py
|
||||
│ ├── conf/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── opts.py (standard oslo.config pattern)
|
||||
│ │ └── main.py
|
||||
│ ├── cli.py (for config export command)
|
||||
│ └── [plugin/extension files]
|
||||
├── tests/
|
||||
│ ├── __init__.py
|
||||
│ └── test_*.py
|
||||
├── docs/
|
||||
├── pyproject.toml
|
||||
├── setup.py
|
||||
├── tox.ini
|
||||
├── .pre-commit-config.yaml
|
||||
├── Makefile
|
||||
└── README.md or README.rst
|
||||
```
|
||||
|
||||
## Key Rules
|
||||
|
||||
- Always use underscores in module names (e.g., `aprsd_joke_plugin`, not `aprsd-joke-plugin`)
|
||||
- Place all configuration in `conf/` subdirectory within the module
|
||||
- Keep tests in top-level `tests/` folder, not nested in module
|
||||
- Use `.venv` for virtual environment
|
||||
- All plugins/extensions should have a `cli.py` for config export
|
||||
- Include both `pyproject.toml` and `setup.py` for maximum compatibility
|
||||
|
||||
## Virtual Environment
|
||||
|
||||
- Always use the virtual environment provided in the project called `.venv`
|
||||
- Never commit `.venv` to git
|
||||
|
||||
## Documentation
|
||||
|
||||
- Maintain docs in `docs/` directory
|
||||
- Use Sphinx for documentation generation
|
||||
- Keep README files up to date with plugin functionality
|
||||
@@ -0,0 +1,209 @@
|
||||
---
|
||||
description: pyproject.toml standards for APRSD plugins and extensions
|
||||
globs: **/pyproject.toml
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# APRSD pyproject.toml Standards
|
||||
|
||||
## Required Entry Points
|
||||
|
||||
All plugins/extensions must define these entry points:
|
||||
|
||||
```toml
|
||||
[project.entry-points."oslo.config.opts"]
|
||||
"module_name.conf" = "module_name.conf.opts:list_opts"
|
||||
|
||||
[project.scripts]
|
||||
"module-name-export-config" = "module_name.cli:main"
|
||||
```
|
||||
|
||||
For plugins with README support:
|
||||
|
||||
```toml
|
||||
[project.entry-points."aprsd.plugin.readme"]
|
||||
"module_name" = "module_name:get_readme"
|
||||
```
|
||||
|
||||
## Standard Dependencies
|
||||
|
||||
All APRSD plugins/extensions should include:
|
||||
|
||||
```toml
|
||||
dependencies = [
|
||||
"aprsd>=4.2.0", # Pin minimum APRSD version
|
||||
"oslo-config", # or "oslo_config" - for configuration management
|
||||
]
|
||||
```
|
||||
|
||||
## Project Metadata
|
||||
|
||||
### Author Information
|
||||
|
||||
Use consistent author format:
|
||||
|
||||
```toml
|
||||
authors = [
|
||||
{name = "Walter A. Boring IV", email = "waboring@hemna.com"},
|
||||
]
|
||||
|
||||
maintainers = [
|
||||
{name = "Walter A. Boring IV", email = "waboring@hemna.com"},
|
||||
]
|
||||
```
|
||||
|
||||
### Keywords
|
||||
|
||||
Include standard APRSD keywords plus project-specific ones:
|
||||
|
||||
```toml
|
||||
keywords = [
|
||||
"aprs",
|
||||
"aprs-is",
|
||||
"aprsd",
|
||||
"aprsd-server",
|
||||
"aprsd-client",
|
||||
"ham-radio",
|
||||
# Add project-specific keywords
|
||||
]
|
||||
```
|
||||
|
||||
### Classifiers
|
||||
|
||||
Include appropriate classifiers:
|
||||
|
||||
```toml
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Topic :: Communications :: Ham Radio",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
]
|
||||
```
|
||||
|
||||
### Python Version
|
||||
|
||||
Set appropriate Python version requirements:
|
||||
|
||||
```toml
|
||||
requires-python = ">=3.10"
|
||||
```
|
||||
|
||||
### License
|
||||
|
||||
```toml
|
||||
license = {file = "LICENSE"}
|
||||
```
|
||||
|
||||
### README
|
||||
|
||||
```toml
|
||||
readme = {file = "README.md", content-type = "text/markdown"}
|
||||
# or
|
||||
readme = {file = "README.rst", content-type = "text/x-rst"}
|
||||
```
|
||||
|
||||
## Tool Configuration
|
||||
|
||||
### setuptools_scm
|
||||
|
||||
For version management:
|
||||
|
||||
```toml
|
||||
[tool.setuptools_scm]
|
||||
|
||||
[project]
|
||||
dynamic = ["version"]
|
||||
```
|
||||
|
||||
### isort
|
||||
|
||||
For import sorting:
|
||||
|
||||
```toml
|
||||
[tool.isort]
|
||||
force_sort_within_sections = true
|
||||
line_length = 88
|
||||
skip_gitignore = true
|
||||
```
|
||||
|
||||
### coverage
|
||||
|
||||
For test coverage:
|
||||
|
||||
```toml
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
```
|
||||
|
||||
### pytest
|
||||
|
||||
For testing configuration:
|
||||
|
||||
```toml
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
norecursedirs = [
|
||||
".tox",
|
||||
".git",
|
||||
"build",
|
||||
"dist",
|
||||
"*.egg-info",
|
||||
".venv",
|
||||
]
|
||||
```
|
||||
|
||||
### ruff (if using)
|
||||
|
||||
For linting and formatting:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
line-length = 99
|
||||
target-version = "py311"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I", "N", "W", "UP"]
|
||||
ignore = ["E203"]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["module_name"]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = "double"
|
||||
indent-style = "space"
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
Use setuptools with setuptools_scm:
|
||||
|
||||
```toml
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=69.5.0",
|
||||
"setuptools_scm>=0",
|
||||
"wheel",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
```
|
||||
|
||||
## Package Configuration
|
||||
|
||||
```toml
|
||||
[tool.setuptools]
|
||||
packages = ["module_name"]
|
||||
# or for auto-discovery:
|
||||
packages = {find = {}}
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"*" = ["LICENSE"]
|
||||
"module_name" = ["README.md"]
|
||||
```
|
||||
@@ -0,0 +1,175 @@
|
||||
---
|
||||
description: Testing standards for APRSD plugins and extensions
|
||||
globs: **/test_*.py
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# APRSD Testing Standards
|
||||
|
||||
## Test Organization
|
||||
|
||||
- Place all tests in top-level `tests/` folder, not nested in module
|
||||
- Name test files `test_*.py`
|
||||
- Use pytest as the test framework
|
||||
- Create `tests/__init__.py` to make tests a package
|
||||
|
||||
## Test Structure
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from aprsd import packets
|
||||
from unittest import mock
|
||||
|
||||
from module_name import MyPlugin
|
||||
|
||||
|
||||
class TestMyPlugin:
|
||||
"""Test cases for MyPlugin."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup test fixtures."""
|
||||
self.plugin = MyPlugin()
|
||||
|
||||
def test_plugin_setup(self):
|
||||
"""Test plugin setup."""
|
||||
self.plugin.setup()
|
||||
# Assertions here
|
||||
|
||||
def test_plugin_process_success(self):
|
||||
"""Test successful message processing."""
|
||||
# Create test packet
|
||||
packet = packets.core.Packet(
|
||||
from_call="TEST",
|
||||
to_call="DEST",
|
||||
message_text="test command",
|
||||
)
|
||||
|
||||
result = self.plugin.process(packet)
|
||||
assert result is not None
|
||||
|
||||
def test_plugin_process_error(self):
|
||||
"""Test error handling."""
|
||||
# Test error cases
|
||||
pass
|
||||
|
||||
@mock.patch("module_name.plugin.requests.get")
|
||||
def test_external_api_call(self, mock_get):
|
||||
"""Test external API calls are mocked."""
|
||||
mock_get.return_value.json.return_value = {"data": "test"}
|
||||
# Test logic here
|
||||
```
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
### Mock External Dependencies
|
||||
|
||||
- Always mock external API calls
|
||||
- Mock network requests using `unittest.mock` or `pytest-mock`
|
||||
- Don't make real network calls in tests
|
||||
|
||||
```python
|
||||
@mock.patch("requests.get")
|
||||
def test_api_call(self, mock_get):
|
||||
mock_get.return_value.json.return_value = {"result": "ok"}
|
||||
# Test here
|
||||
```
|
||||
|
||||
### Test Both Success and Failure Cases
|
||||
|
||||
- Test happy path (successful operations)
|
||||
- Test error handling (exceptions, invalid input, etc.)
|
||||
- Test edge cases (empty strings, None values, etc.)
|
||||
|
||||
### Use Fixtures
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def sample_packet():
|
||||
"""Create a sample test packet."""
|
||||
return packets.core.Packet(
|
||||
from_call="TEST",
|
||||
to_call="DEST",
|
||||
message_text="test",
|
||||
)
|
||||
|
||||
def test_with_fixture(sample_packet):
|
||||
"""Test using fixture."""
|
||||
result = process(sample_packet)
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
### Test Configuration
|
||||
|
||||
Test that configuration is properly loaded:
|
||||
|
||||
```python
|
||||
def test_config_loading(self):
|
||||
"""Test configuration options."""
|
||||
from module_name.conf import main
|
||||
opts = main.list_opts()
|
||||
assert "my_plugin" in opts
|
||||
```
|
||||
|
||||
## pytest Configuration
|
||||
|
||||
Include in `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
norecursedirs = [
|
||||
".tox",
|
||||
".git",
|
||||
"build",
|
||||
"dist",
|
||||
"*.egg-info",
|
||||
".venv",
|
||||
"venv",
|
||||
]
|
||||
```
|
||||
|
||||
## Coverage Configuration
|
||||
|
||||
Include in `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
|
||||
[tool.coverage.report]
|
||||
exclude_lines = [
|
||||
"pragma: no cover",
|
||||
"def __repr__",
|
||||
"raise AssertionError",
|
||||
"raise NotImplementedError",
|
||||
"if __name__ == .__main__.:",
|
||||
]
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
Tests should be runnable with:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# Run with coverage
|
||||
pytest --cov=module_name
|
||||
|
||||
# Run specific test
|
||||
pytest tests/test_specific.py::TestClass::test_method
|
||||
```
|
||||
|
||||
## What to Test
|
||||
|
||||
- Plugin setup and initialization
|
||||
- Message processing logic
|
||||
- Configuration loading
|
||||
- Error handling
|
||||
- Helper functions
|
||||
- Thread creation (if applicable)
|
||||
- CLI commands
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
description: my rules for aprsd
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Coding pattern preferences
|
||||
|
||||
- Always prefer simple solutions
|
||||
- Avoid duplication of code wherever possible, which means checking for other areas of the codebase that might already have similar code and functionality
|
||||
- you are careful to only make changes that are requires or you are confident are well understood and related to the change being requested
|
||||
- when fixing an issue or bug, do not introduce a new patter or technology without first exhausting all options for the exhausting implementation. And if you finally do this, make sure to remove the old implementation afterwards so we don't have duplicate logic
|
||||
- keep the codebase very clean and organized
|
||||
- Avoid writing scripts in files if possible, especially if the script is likely only to be run once.
|
||||
- avoid having files over 200-300 lines of code. Refactor at that point.
|
||||
- Mocking data is only needed for tests, never mock data for dev or prod
|
||||
- Never add stubbing or fake data patterns to code that affects the dev or prod environments
|
||||
- never overwrite my .env file without first asking and confirming.
|
||||
- All threads should be done with APRSDThread base class
|
||||
- place all unit tests in the top level aprsd/tests folder
|
||||
- Always use the virtual environment provided in the project called .venv
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
description: This is the project's tech stack
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# Technical stack
|
||||
- Python
|
||||
- Python tests
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
description:
|
||||
globs:
|
||||
alwaysApply: true
|
||||
---
|
||||
# Coding workflow preferences
|
||||
|
||||
- Focus on areas of code relevant to the task
|
||||
- Do not touch code unrelated to the task
|
||||
- write thorough tests for all major functionality
|
||||
- Avoid making major changes to the patterns and architecture of how a feature works, after it was shown to work well, unless explicitly instructed
|
||||
- Always think about what other methods and areas of code might be affected by code changes
|
||||
@@ -0,0 +1,684 @@
|
||||
# APRSD GitHub Copilot Instructions
|
||||
|
||||
This file provides coding standards and patterns for all APRSD plugins, extensions, and the main APRSD project.
|
||||
|
||||
## General Coding Principles
|
||||
|
||||
- Always prefer simple solutions over complex ones
|
||||
- Avoid code duplication - check for existing similar functionality before adding new code
|
||||
- Keep the codebase very clean and organized
|
||||
- Avoid having files over 200-300 lines of code - refactor at that point
|
||||
- Only make changes that are required or well understood and related to the change being requested
|
||||
- When fixing bugs, don't introduce new patterns without exhausting existing implementation options first
|
||||
- Mocking data is only needed for tests, never mock data for dev or prod environments
|
||||
- Never add stubbing or fake data patterns to code that affects dev or prod environments
|
||||
- Never overwrite `.env` files without first asking and confirming
|
||||
- Always use the virtual environment provided in the project called `.venv`
|
||||
- Focus on areas of code relevant to the task - don't touch unrelated code
|
||||
- Write thorough tests for all major functionality
|
||||
|
||||
---
|
||||
|
||||
## Project Structure Standards
|
||||
|
||||
All APRSD plugins and extensions must follow this structure:
|
||||
|
||||
```
|
||||
project-name/
|
||||
├── module_name/ # Use underscores, not hyphens
|
||||
│ ├── __init__.py
|
||||
│ ├── conf/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── opts.py # Standard oslo.config pattern
|
||||
│ │ └── main.py
|
||||
│ ├── cli.py # For config export command
|
||||
│ └── [plugin/extension files]
|
||||
├── tests/ # Top-level tests folder
|
||||
│ ├── __init__.py
|
||||
│ └── test_*.py
|
||||
├── docs/
|
||||
├── pyproject.toml
|
||||
├── setup.py
|
||||
├── tox.ini
|
||||
├── .pre-commit-config.yaml
|
||||
├── Makefile
|
||||
└── README.md or README.rst
|
||||
```
|
||||
|
||||
**Key Requirements:**
|
||||
- Module names use underscores (e.g., `aprsd_joke_plugin`, not `aprsd-joke-plugin`)
|
||||
- All configuration goes in `conf/` subdirectory within the module
|
||||
- Tests in top-level `tests/` folder, not nested in module
|
||||
- Use `.venv` for virtual environment
|
||||
- All plugins/extensions have a `cli.py` for config export
|
||||
- Include both `pyproject.toml` and `setup.py` for maximum compatibility
|
||||
|
||||
---
|
||||
|
||||
## Plugin Development Patterns
|
||||
|
||||
### Plugin Base Class
|
||||
|
||||
All plugins must inherit from `plugin.APRSDRegexCommandPluginBase`:
|
||||
|
||||
```python
|
||||
from aprsd import plugin, packets
|
||||
from aprsd.utils import trace
|
||||
import logging
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
class MyPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Plugin description.
|
||||
|
||||
Explain what the plugin does, what commands it responds to,
|
||||
and any special configuration needed.
|
||||
"""
|
||||
|
||||
version = module_name.__version__
|
||||
command_regex = "^[cC]" # Regex to match command
|
||||
command_name = "mycommand" # One-word command name
|
||||
enabled = False
|
||||
```
|
||||
|
||||
### Required Plugin Methods
|
||||
|
||||
#### setup()
|
||||
Initialize plugin and set `self.enabled` flag:
|
||||
|
||||
```python
|
||||
def setup(self):
|
||||
"""Allows the plugin to do some 'setup' type checks.
|
||||
|
||||
If the setup checks fail, set self.enabled = False. This
|
||||
will prevent the plugin from being called when packets are received.
|
||||
"""
|
||||
self.enabled = CONF.my_plugin.enabled
|
||||
# Additional setup checks here
|
||||
```
|
||||
|
||||
#### process()
|
||||
Must use `@trace.trace` decorator and accept `Packet`:
|
||||
|
||||
```python
|
||||
@trace.trace
|
||||
def process(self, packet: packets.core.Packet):
|
||||
"""Process matching packet.
|
||||
|
||||
This is called when a received packet matches self.command_regex.
|
||||
Only called when self.enabled = True.
|
||||
"""
|
||||
if not self.enabled:
|
||||
LOG.info("Plugin is not enabled")
|
||||
return
|
||||
|
||||
message = packet.message_text
|
||||
|
||||
# Your logic here
|
||||
|
||||
return "response message"
|
||||
```
|
||||
|
||||
#### help()
|
||||
Return list of help strings:
|
||||
|
||||
```python
|
||||
def help(self):
|
||||
"""Return help message for the plugin."""
|
||||
return [
|
||||
f"{self.command_name}: Description of what command does",
|
||||
"Usage: command [options]",
|
||||
"Additional help lines if needed"
|
||||
]
|
||||
```
|
||||
|
||||
#### create_threads()
|
||||
Return list of APRSDThread objects (or empty list):
|
||||
|
||||
```python
|
||||
def create_threads(self):
|
||||
"""Create and return custom APRSDThread objects.
|
||||
|
||||
Create a child of aprsd.threads.APRSDThread object and return it.
|
||||
It will automatically get started.
|
||||
"""
|
||||
if self.enabled:
|
||||
# return [MyAPRSDThread()]
|
||||
return []
|
||||
```
|
||||
|
||||
### Plugin Best Practices
|
||||
|
||||
**Message Formatting:**
|
||||
- Wrap long messages using `textwrap.wrap(text, 67, break_long_words=False)`
|
||||
- APRS messages have a 67 character limit per line
|
||||
- Return strings or lists of strings from `process()`
|
||||
|
||||
**Error Handling:**
|
||||
- Always wrap external API calls in try/except blocks
|
||||
- Log errors with `LOG.error()`
|
||||
- Return user-friendly error messages
|
||||
- Don't let exceptions bubble up from `process()`
|
||||
|
||||
```python
|
||||
try:
|
||||
result = external_api_call()
|
||||
except Exception as e:
|
||||
LOG.error(f"Error calling API: {e}")
|
||||
return "Error: Unable to fetch data"
|
||||
```
|
||||
|
||||
**Logging:**
|
||||
- Use `LOG = logging.getLogger("APRSD")` at module level
|
||||
- Use appropriate log levels: DEBUG, INFO, WARNING, ERROR
|
||||
- Include context in log messages
|
||||
|
||||
---
|
||||
|
||||
## Extension Development Patterns
|
||||
|
||||
### Extension Base Class
|
||||
|
||||
Extensions provide additional functionality beyond message processing:
|
||||
|
||||
```python
|
||||
from aprsd import plugin
|
||||
|
||||
class MyExtension(plugin.APRSDExtensionBase):
|
||||
"""Extension description.
|
||||
|
||||
Extensions can add web interfaces, commands, background services, etc.
|
||||
"""
|
||||
|
||||
version = module_name.__version__
|
||||
```
|
||||
|
||||
### Extension Types
|
||||
|
||||
**Web Extensions** (like aprsd-admin-extension, aprsd-webchat-extension):
|
||||
```python
|
||||
def setup(self):
|
||||
"""Setup the extension."""
|
||||
self.enabled = CONF.my_extension.enabled
|
||||
|
||||
def create_threads(self):
|
||||
"""Create web server or other background threads."""
|
||||
if self.enabled:
|
||||
return [MyWebServerThread()]
|
||||
return []
|
||||
```
|
||||
|
||||
**CLI Extensions** (like aprsd-trip-extension):
|
||||
- Add commands in `cmds/` directory
|
||||
- Register entry points in pyproject.toml
|
||||
- Follow Click or argparse patterns consistently
|
||||
|
||||
**Background Service Extensions:**
|
||||
- All threads must inherit from `APRSDThread` base class
|
||||
- Implement proper thread lifecycle methods
|
||||
- Handle graceful shutdown
|
||||
|
||||
---
|
||||
|
||||
## Configuration Patterns (Oslo.config)
|
||||
|
||||
### conf/opts.py Structure
|
||||
|
||||
Must include Apache license header and follow this standard pattern:
|
||||
|
||||
```python
|
||||
# Copyright 2015 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
import collections
|
||||
import importlib
|
||||
import importlib.util
|
||||
import os
|
||||
import pkgutil
|
||||
|
||||
LIST_OPTS_FUNC_NAME = "list_opts"
|
||||
|
||||
def list_opts():
|
||||
"""Collect all configuration options from conf modules."""
|
||||
opts = collections.defaultdict(list)
|
||||
module_names = _list_module_names()
|
||||
imported_modules = _import_modules(module_names)
|
||||
_append_config_options(imported_modules, opts)
|
||||
return _tupleize(opts)
|
||||
|
||||
def export_config(format="dict"):
|
||||
"""Export configuration options as dict or JSON.
|
||||
|
||||
Returns configuration metadata including type, default, help text, etc.
|
||||
"""
|
||||
# Standard export implementation
|
||||
# See existing plugins for complete implementation
|
||||
```
|
||||
|
||||
### conf/main.py Pattern
|
||||
|
||||
Define plugin/extension-specific config options:
|
||||
|
||||
```python
|
||||
from oslo_config import cfg
|
||||
|
||||
# Define configuration group
|
||||
plugin_group = cfg.OptGroup(
|
||||
name="my_plugin",
|
||||
title="My Plugin Settings",
|
||||
)
|
||||
|
||||
# Define configuration options
|
||||
plugin_opts = [
|
||||
cfg.BoolOpt(
|
||||
"enabled",
|
||||
default=False,
|
||||
help="Enable the plugin",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"api_key",
|
||||
default=None,
|
||||
help="API key for external service",
|
||||
secret=True,
|
||||
),
|
||||
cfg.IntOpt(
|
||||
"timeout",
|
||||
default=30,
|
||||
help="Timeout in seconds for API calls",
|
||||
min=1,
|
||||
max=300,
|
||||
),
|
||||
]
|
||||
|
||||
def register_opts(config):
|
||||
"""Register configuration options."""
|
||||
config.register_group(plugin_group)
|
||||
config.register_opts(plugin_opts, group=plugin_group)
|
||||
|
||||
def list_opts():
|
||||
"""Return configuration options."""
|
||||
return {plugin_group.name: plugin_opts}
|
||||
```
|
||||
|
||||
### Configuration Best Practices
|
||||
|
||||
- Always include an `enabled` boolean option
|
||||
- Mark sensitive options with `secret=True`
|
||||
- Provide reasonable defaults
|
||||
- Include helpful documentation in help strings
|
||||
- Use appropriate option types (StrOpt, BoolOpt, IntOpt, ListOpt, etc.)
|
||||
- Set min/max bounds for numeric values
|
||||
- Use choices for enum-like options
|
||||
|
||||
### Accessing Configuration
|
||||
|
||||
```python
|
||||
from oslo_config import cfg
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
# Access config values
|
||||
if CONF.my_plugin.enabled:
|
||||
api_key = CONF.my_plugin.api_key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## pyproject.toml Standards
|
||||
|
||||
### Required Entry Points
|
||||
|
||||
```toml
|
||||
[project.entry-points."oslo.config.opts"]
|
||||
"module_name.conf" = "module_name.conf.opts:list_opts"
|
||||
|
||||
[project.scripts]
|
||||
"module-name-export-config" = "module_name.cli:main"
|
||||
```
|
||||
|
||||
For plugins with README support:
|
||||
```toml
|
||||
[project.entry-points."aprsd.plugin.readme"]
|
||||
"module_name" = "module_name:get_readme"
|
||||
```
|
||||
|
||||
### Standard Dependencies
|
||||
|
||||
```toml
|
||||
dependencies = [
|
||||
"aprsd>=4.2.0", # Pin minimum APRSD version
|
||||
"oslo-config", # or "oslo_config" - for configuration management
|
||||
]
|
||||
```
|
||||
|
||||
### Project Metadata
|
||||
|
||||
**Author Information:**
|
||||
```toml
|
||||
authors = [
|
||||
{name = "Walter A. Boring IV", email = "waboring@hemna.com"},
|
||||
]
|
||||
maintainers = [
|
||||
{name = "Walter A. Boring IV", email = "waboring@hemna.com"},
|
||||
]
|
||||
```
|
||||
|
||||
**Keywords:**
|
||||
```toml
|
||||
keywords = [
|
||||
"aprs",
|
||||
"aprs-is",
|
||||
"aprsd",
|
||||
"ham-radio",
|
||||
# Add project-specific keywords
|
||||
]
|
||||
```
|
||||
|
||||
**Classifiers:**
|
||||
```toml
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"Topic :: Communications :: Ham Radio",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
]
|
||||
```
|
||||
|
||||
**Python Version:**
|
||||
```toml
|
||||
requires-python = ">=3.10"
|
||||
```
|
||||
|
||||
### Tool Configurations
|
||||
|
||||
```toml
|
||||
[tool.setuptools_scm]
|
||||
|
||||
[tool.isort]
|
||||
force_sort_within_sections = true
|
||||
line_length = 88
|
||||
skip_gitignore = true
|
||||
|
||||
[tool.coverage.run]
|
||||
branch = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
norecursedirs = [".tox", ".git", "build", "dist", "*.egg-info", ".venv"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI Command Patterns
|
||||
|
||||
### Config Export Command (cli.py)
|
||||
|
||||
All plugins/extensions must provide a config export CLI command:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""CLI tool for module-name configuration export."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
def export_config_cmd(format="json"):
|
||||
"""Export plugin configuration options."""
|
||||
try:
|
||||
from module_name.conf.opts import export_config
|
||||
|
||||
result = export_config(format=format)
|
||||
|
||||
if format == "json":
|
||||
print(result)
|
||||
else:
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
return 0
|
||||
except ImportError as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
print("\nTo export config, install oslo.config:", file=sys.stderr)
|
||||
print(" pip install oslo.config", file=sys.stderr)
|
||||
return 1
|
||||
except Exception as e:
|
||||
print(f"Error exporting config: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
def main():
|
||||
"""Main entry point for CLI."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Export module-name configuration options"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=["dict", "json"],
|
||||
default="json",
|
||||
help="Output format (default: json)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
sys.exit(export_config_cmd(format=args.format))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
### CLI Best Practices
|
||||
|
||||
- Use argparse for command-line parsing
|
||||
- Provide clear help messages
|
||||
- Return appropriate exit codes (0 for success, non-zero for errors)
|
||||
- Write errors to stderr using `file=sys.stderr`
|
||||
- Handle ImportError gracefully with helpful messages
|
||||
- Include shebang `#!/usr/bin/env python3` at the top
|
||||
|
||||
### Command Naming Convention
|
||||
|
||||
- Use hyphenated names (e.g., `aprsd-joke-plugin-export-config`)
|
||||
- Start with the module name
|
||||
- End with the action (e.g., `export-config`)
|
||||
- Keep names descriptive but concise
|
||||
|
||||
---
|
||||
|
||||
## Testing Standards
|
||||
|
||||
### Test Organization
|
||||
|
||||
- Place all tests in top-level `tests/` folder, not nested in module
|
||||
- Name test files `test_*.py`
|
||||
- Use pytest as the test framework
|
||||
- Create `tests/__init__.py` to make tests a package
|
||||
|
||||
### Test Structure
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from aprsd import packets
|
||||
from unittest import mock
|
||||
|
||||
from module_name import MyPlugin
|
||||
|
||||
class TestMyPlugin:
|
||||
"""Test cases for MyPlugin."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup test fixtures."""
|
||||
self.plugin = MyPlugin()
|
||||
|
||||
def test_plugin_setup(self):
|
||||
"""Test plugin setup."""
|
||||
self.plugin.setup()
|
||||
# Assertions here
|
||||
|
||||
def test_plugin_process_success(self):
|
||||
"""Test successful message processing."""
|
||||
packet = packets.core.Packet(
|
||||
from_call="TEST",
|
||||
to_call="DEST",
|
||||
message_text="test command",
|
||||
)
|
||||
result = self.plugin.process(packet)
|
||||
assert result is not None
|
||||
|
||||
@mock.patch("module_name.plugin.requests.get")
|
||||
def test_external_api_call(self, mock_get):
|
||||
"""Test external API calls are mocked."""
|
||||
mock_get.return_value.json.return_value = {"data": "test"}
|
||||
# Test logic here
|
||||
```
|
||||
|
||||
### Testing Best Practices
|
||||
|
||||
**Mock External Dependencies:**
|
||||
- Always mock external API calls
|
||||
- Mock network requests using `unittest.mock` or `pytest-mock`
|
||||
- Don't make real network calls in tests
|
||||
|
||||
**Test Both Success and Failure Cases:**
|
||||
- Test happy path (successful operations)
|
||||
- Test error handling (exceptions, invalid input, etc.)
|
||||
- Test edge cases (empty strings, None values, etc.)
|
||||
|
||||
**Use Fixtures:**
|
||||
```python
|
||||
@pytest.fixture
|
||||
def sample_packet():
|
||||
"""Create a sample test packet."""
|
||||
return packets.core.Packet(
|
||||
from_call="TEST",
|
||||
to_call="DEST",
|
||||
message_text="test",
|
||||
)
|
||||
|
||||
def test_with_fixture(sample_packet):
|
||||
"""Test using fixture."""
|
||||
result = process(sample_packet)
|
||||
assert result is not None
|
||||
```
|
||||
|
||||
### What to Test
|
||||
|
||||
- Plugin setup and initialization
|
||||
- Message processing logic
|
||||
- Configuration loading
|
||||
- Error handling
|
||||
- Helper functions
|
||||
- Thread creation (if applicable)
|
||||
- CLI commands
|
||||
|
||||
---
|
||||
|
||||
## Thread Management
|
||||
|
||||
### APRSDThread Base Class
|
||||
|
||||
All background threads must inherit from `APRSDThread`:
|
||||
|
||||
```python
|
||||
from aprsd.threads import APRSDThread
|
||||
|
||||
class MyThread(APRSDThread):
|
||||
"""Background thread description."""
|
||||
|
||||
def __init__(self, config):
|
||||
super().__init__("MyThread")
|
||||
self.config = config
|
||||
|
||||
def loop(self):
|
||||
"""Main thread loop logic."""
|
||||
# Your periodic logic here
|
||||
return True # Return True to continue, False to stop
|
||||
```
|
||||
|
||||
**Key Points:**
|
||||
- Place all thread implementations using APRSDThread base class
|
||||
- Implement the `loop()` method for periodic operations
|
||||
- Handle graceful shutdown properly
|
||||
- Use appropriate logging
|
||||
|
||||
---
|
||||
|
||||
## Thread-Specific Guidelines
|
||||
|
||||
When implementing threads:
|
||||
- All threads should be done with APRSDThread base class
|
||||
- Implement proper lifecycle methods
|
||||
- Handle exceptions within the thread
|
||||
- Log thread startup and shutdown
|
||||
- Use configuration for thread intervals
|
||||
|
||||
---
|
||||
|
||||
## Additional Guidelines
|
||||
|
||||
### Documentation
|
||||
|
||||
- Maintain docs in `docs/` directory
|
||||
- Use Sphinx for documentation generation
|
||||
- Keep README files up to date with plugin functionality
|
||||
- Document configuration options in docstrings
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
- Use `.pre-commit-config.yaml` for code quality
|
||||
- Include linting, formatting, and type checking
|
||||
- Run pre-commit hooks before committing
|
||||
|
||||
### Build System
|
||||
|
||||
```toml
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=69.5.0",
|
||||
"setuptools_scm>=0",
|
||||
"wheel",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
```
|
||||
|
||||
### Package Configuration
|
||||
|
||||
```toml
|
||||
[tool.setuptools]
|
||||
packages = ["module_name"]
|
||||
# or for auto-discovery:
|
||||
packages = {find = {}}
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"*" = ["LICENSE"]
|
||||
"module_name" = ["README.md"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
When developing APRSD plugins and extensions:
|
||||
|
||||
1. **Follow the standard project structure** with underscored module names
|
||||
2. **Inherit from the correct base classes** (APRSDRegexCommandPluginBase or APRSDExtensionBase)
|
||||
3. **Implement required methods** (setup, process, help, create_threads)
|
||||
4. **Use oslo.config** for all configuration management
|
||||
5. **Provide CLI commands** for config export
|
||||
6. **Write comprehensive tests** with proper mocking
|
||||
7. **Handle errors gracefully** with appropriate logging
|
||||
8. **Format APRS messages** to 67 character limit
|
||||
9. **Use APRSDThread** for all background threads
|
||||
10. **Maintain clean, simple, DRY code**
|
||||
|
||||
These standards ensure consistency, maintainability, and quality across all APRSD projects.
|
||||
Generated
+3
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.8 (aprsd-twitter-plugin)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
<component name="TestRunnerService">
|
||||
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="accountSettings">
|
||||
<option name="activeProfile" value="profile:default" />
|
||||
<option name="activeRegion" value="us-east-1" />
|
||||
<option name="recentlyUsedProfiles">
|
||||
<list>
|
||||
<option value="profile:default" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="recentlyUsedRegions">
|
||||
<list>
|
||||
<option value="us-east-1" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
Generated
+4
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (aprsd-twitter-plugin)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/aprsd-twitter-plugin.iml" filepath="$PROJECT_DIR$/.idea/aprsd-twitter-plugin.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
#
|
||||
# SEAMLESSLY MANAGE PYTHON VIRTUAL ENVIRONMENT WITH A MAKEFILE
|
||||
#
|
||||
# https://github.com/sio/Makefile.venv v2020.08.14
|
||||
#
|
||||
#
|
||||
# Insert `include Makefile.venv` at the bottom of your Makefile to enable these
|
||||
# rules.
|
||||
#
|
||||
# When writing your Makefile use '$(VENV)/python' to refer to the Python
|
||||
# interpreter within virtual environment and '$(VENV)/executablename' for any
|
||||
# other executable in venv.
|
||||
#
|
||||
# This Makefile provides the following targets:
|
||||
# venv
|
||||
# Use this as a dependency for any target that requires virtual
|
||||
# environment to be created and configured
|
||||
# python, ipython
|
||||
# Use these to launch interactive Python shell within virtual environment
|
||||
# shell, bash, zsh
|
||||
# Launch interactive command line shell. "shell" target launches the
|
||||
# default shell Makefile executes its rules in (usually /bin/sh).
|
||||
# "bash" and "zsh" can be used to refer to the specific desired shell.
|
||||
# show-venv
|
||||
# Show versions of Python and pip, and the path to the virtual environment
|
||||
# clean-venv
|
||||
# Remove virtual environment
|
||||
# $(VENV)/executable_name
|
||||
# Install `executable_name` with pip. Only packages with names matching
|
||||
# the name of the corresponding executable are supported.
|
||||
# Use this as a lightweight mechanism for development dependencies
|
||||
# tracking. E.g. for one-off tools that are not required in every
|
||||
# developer's environment, therefore are not included into
|
||||
# requirements.txt or setup.py.
|
||||
# Note:
|
||||
# Rules using such target or dependency MUST be defined below
|
||||
# `include` directive to make use of correct $(VENV) value.
|
||||
# Example:
|
||||
# codestyle: $(VENV)/pyflakes
|
||||
# $(VENV)/pyflakes .
|
||||
# See `ipython` target below for another example.
|
||||
#
|
||||
# This Makefile can be configured via following variables:
|
||||
# PY
|
||||
# Command name for system Python interpreter. It is used only initialy to
|
||||
# create the virtual environment
|
||||
# Default: python3
|
||||
# REQUIREMENTS_TXT
|
||||
# Space separated list of paths to requirements.txt files.
|
||||
# Paths are resolved relative to current working directory.
|
||||
# Default: requirements.txt
|
||||
# WORKDIR
|
||||
# Parent directory for the virtual environment.
|
||||
# Default: current working directory.
|
||||
# VENVDIR
|
||||
# Python virtual environment directory.
|
||||
# Default: $(WORKDIR)/.venv
|
||||
#
|
||||
# This Makefile was written for GNU Make and may not work with other make
|
||||
# implementations.
|
||||
#
|
||||
#
|
||||
# Copyright (c) 2019-2020 Vitaly Potyarkin
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0
|
||||
# <http://www.apache.org/licenses/LICENSE-2.0>
|
||||
#
|
||||
|
||||
|
||||
#
|
||||
# Configuration variables
|
||||
#
|
||||
|
||||
PY?=python3
|
||||
WORKDIR?=.
|
||||
VENVDIR?=$(WORKDIR)/.venv
|
||||
REQUIREMENTS_TXT?=$(wildcard requirements.txt) # Multiple paths are supported (space separated)
|
||||
MARKER=.initialized-with-Makefile.venv
|
||||
|
||||
|
||||
#
|
||||
# Internal variable resolution
|
||||
#
|
||||
|
||||
VENV=$(VENVDIR)/bin
|
||||
EXE=
|
||||
# Detect windows
|
||||
ifeq (win32,$(shell $(PY) -c "import __future__, sys; print(sys.platform)"))
|
||||
VENV=$(VENVDIR)/Scripts
|
||||
EXE=.exe
|
||||
endif
|
||||
|
||||
|
||||
#
|
||||
# Virtual environment
|
||||
#
|
||||
|
||||
.PHONY: venv
|
||||
venv: $(VENV)/$(MARKER)
|
||||
|
||||
.PHONY: clean-venv
|
||||
clean-venv:
|
||||
-$(RM) -r "$(VENVDIR)"
|
||||
|
||||
.PHONY: show-venv
|
||||
show-venv: venv
|
||||
@$(VENV)/python -c "import sys; print('Python ' + sys.version.replace('\n',''))"
|
||||
@$(VENV)/pip --version
|
||||
@echo venv: $(VENVDIR)
|
||||
|
||||
.PHONY: debug-venv
|
||||
debug-venv:
|
||||
@$(MAKE) --version
|
||||
$(info PY="$(PY)")
|
||||
$(info REQUIREMENTS_TXT="$(REQUIREMENTS_TXT)")
|
||||
$(info VENVDIR="$(VENVDIR)")
|
||||
$(info VENVDEPENDS="$(VENVDEPENDS)")
|
||||
$(info WORKDIR="$(WORKDIR)")
|
||||
|
||||
|
||||
#
|
||||
# Dependencies
|
||||
#
|
||||
|
||||
ifneq ($(strip $(REQUIREMENTS_TXT)),)
|
||||
VENVDEPENDS+=$(REQUIREMENTS_TXT)
|
||||
endif
|
||||
|
||||
ifneq ($(wildcard setup.py),)
|
||||
VENVDEPENDS+=setup.py
|
||||
endif
|
||||
ifneq ($(wildcard setup.cfg),)
|
||||
VENVDEPENDS+=setup.cfg
|
||||
endif
|
||||
|
||||
$(VENV):
|
||||
$(PY) -m venv $(VENVDIR)
|
||||
$(VENV)/python -m pip install --upgrade pip setuptools wheel
|
||||
|
||||
$(VENV)/$(MARKER): $(VENVDEPENDS) | $(VENV)
|
||||
ifneq ($(strip $(REQUIREMENTS_TXT)),)
|
||||
$(VENV)/pip install $(foreach path,$(REQUIREMENTS_TXT),-r $(path))
|
||||
endif
|
||||
ifneq ($(wildcard setup.py),)
|
||||
$(VENV)/pip install -e .
|
||||
endif
|
||||
touch $(VENV)/$(MARKER)
|
||||
|
||||
|
||||
#
|
||||
# Interactive shells
|
||||
#
|
||||
|
||||
.PHONY: python
|
||||
python: venv
|
||||
exec $(VENV)/python
|
||||
|
||||
.PHONY: ipython
|
||||
ipython: $(VENV)/ipython
|
||||
exec $(VENV)/ipython
|
||||
|
||||
.PHONY: shell
|
||||
shell: venv
|
||||
. $(VENV)/activate && exec $(notdir $(SHELL))
|
||||
|
||||
.PHONY: bash zsh
|
||||
bash zsh: venv
|
||||
. $(VENV)/activate && exec $@
|
||||
|
||||
|
||||
#
|
||||
# Commandline tools (wildcard rule, executable name must match package name)
|
||||
#
|
||||
|
||||
ifneq ($(EXE),)
|
||||
$(VENV)/%: $(VENV)/%$(EXE) ;
|
||||
.PHONY: $(VENV)/%
|
||||
.PRECIOUS: $(VENV)/%$(EXE)
|
||||
endif
|
||||
|
||||
$(VENV)/%$(EXE): $(VENV)/$(MARKER)
|
||||
$(VENV)/pip install --upgrade $*
|
||||
touch $@
|
||||
@@ -0,0 +1,42 @@
|
||||
ham:
|
||||
callsign: WB4BOR
|
||||
|
||||
aprs:
|
||||
login: WB4BOR-13
|
||||
password: XXXXXXXXXX
|
||||
host: noam.aprs2.net
|
||||
port: 14580
|
||||
logfile: /tmp/aprsd.log
|
||||
|
||||
aprsd:
|
||||
trace: True
|
||||
|
||||
watch_list:
|
||||
enabled: False
|
||||
alert_time_seconds: 9600
|
||||
alert_callsign: WB4BOR
|
||||
packet_keep_count: 25
|
||||
|
||||
web:
|
||||
enabled: True
|
||||
logging_enabled: False
|
||||
host: '0.0.0.0'
|
||||
port: 8013
|
||||
users:
|
||||
admin: XXXXXXXXXXX
|
||||
email:
|
||||
enabled: False
|
||||
|
||||
enabled_plugins:
|
||||
- aprsd_twitter_plugin.twitter.SendTweetPlugin
|
||||
|
||||
units: imperial
|
||||
|
||||
services:
|
||||
aprs.fi:
|
||||
apiKey: XXXXxxxXXXXXXXXXXxxXXxX
|
||||
twitter:
|
||||
apiKey: XXXXXXXXXXXXXXXXXXXXXXXXx
|
||||
apiKey_secret: XXXXXXXxxxxxxxxXXXXXXxXXxXxXxXxXxXxXxXxXxXxXXxXxXxXx
|
||||
access_token: XXXXXxxxXXXXxXxXxXXXXxXxXXxXxXXxXxXxxXxXxXxxXxXxXxXx
|
||||
access_token_secret: XXXxxxXxXXXXxXxXxXXxXxXxxXxXxXXxXxXXxXxXxXXxxxXxxx
|
||||
@@ -1,47 +0,0 @@
|
||||
[metadata]
|
||||
name = aprsd_twitter_plugin
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
author = Walter A. Boring IV
|
||||
author_email = waboring@hemna.com
|
||||
license = MIT
|
||||
license_file = LICENSE
|
||||
classifiers =
|
||||
License :: OSI Approved :: MIT License
|
||||
classifier =
|
||||
Topic :: Communications :: Ham Radio
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
description_file =
|
||||
README.rst
|
||||
summary = Python APRSD plugin to send tweets
|
||||
|
||||
[options.entry_points]
|
||||
oslo.config.opts =
|
||||
aprsd_twitter_plugin.conf = aprsd_twitter_plugin.conf.opts:list_opts
|
||||
|
||||
[global]
|
||||
setup-hooks =
|
||||
pbr.hooks.setup_hook
|
||||
|
||||
[files]
|
||||
packages =
|
||||
aprsd_twitter_plugin
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
all_files = 1
|
||||
|
||||
[upload_sphinx]
|
||||
upload-dir = doc/build/html
|
||||
|
||||
[mypy]
|
||||
ignore_missing_imports = True
|
||||
strict = True
|
||||
|
||||
[bdist_wheel]
|
||||
universal = 1
|
||||
Reference in New Issue
Block a user