mirror of
https://github.com/NewFuture/DDNS.git
synced 2026-10-21 04:34:56 +00:00
13 KiB
13 KiB
applyTo
| applyTo |
|---|
| **/*.py |
Python Coding Standards and Best Practices
Core Principles
Gihub Copilot Agent Usage
- Minimize Terminal Usage: Use terminal commands sparingly to reduce complexity
- Windows Compatibility: Avoid
&&and||operators that may not work on all shells - No Directory Changes: Avoid
cdcommands; you are always in the project root - terminal Usage: run command
python3instead ofpython, and this is the only command you should use
Dependencies and Environment
- Standard Library Only: Use only Python standard library modules unless explicitly permitted
- Self-Contained: Code must run without third-party dependencies on Windows, macOS, and Linux
- No External Dependencies: Avoid pip packages to ensure maximum compatibility and easy deployment
Python 2.7 and 3.x Compatibility
- Primary Target: Python 3.x is preferred
- Legacy Support: When Python 2.7 compatibility is required, use
sixlibrary patterns - Forbidden Features:
- NO f-strings (not supported in Python 2.7)
- NO
async/awaitsyntax - Avoid Python 3.6+ exclusive features when compatibility is needed
- Unicode String Handling:
- IMPORTANT: Ruff formatter automatically converts
u"string"to"string" - In Python 2,
u"\n"and"\n"are different types (unicode vs bytes) - Prefer
# fmt: skipon the exact line that must preserve a Unicode literal - Example:
unicode_string = u"hello\nworld" # fmt: skip
- IMPORTANT: Ruff formatter automatically converts
Project Architecture
Directory Structure
ddns/ # Main application code
├── provider/ # DNS provider implementations
│ ├── _base.py # Abstract base classes (SimpleProvider, BaseProvider)
│ └── *.py # Provider-specific implementations
├── util/ # Utility functions and classes
│ ├── http.py # HTTP client functionality
│ ├── config.py # Configuration management
│ └── *.py # Other utilities
└── __init__.py # Package initialization
tests/ # Unit tests
├── base_test.py # Shared test utilities and base classes
├── test_provider_*.py # Provider-specific tests
└── README.md # Testing documentation
doc/ # Documentation
├── cli.md # Command line interface documentation
├── docker.md # Docker usage and deployment guide
├── env.md # Environment variables reference
├── json.md # JSON configuration format
├── dev/ # Developer documentation
│ └── provider.md # Provider development guide
├── providers/ # Provider-specific documentation
│ ├── dnspod.md # DNSPod configuration guide
│ ├── cloudflare.md # Cloudflare configuration guide
│ └── *.md # Other provider guides
└── img/ # Documentation images and diagrams
├── ddns.png # Project logo and icons
└── ddns.svg # Vector graphics and diagrams
schema/ # JSON schemas
├── v2.8.json # Legacy configuration schema v2.8
├── v2.json # Legacy configuration schema v2
└── v4.0.json # Current configuration schema v4.0
Provider Architecture
SimpleProvider (Basic DNS Provider)
Purpose: For DNS providers that only support simple record updates without querying existing records.
Must Implement:
set_record(domain, value, record_type="A", ttl=None, line=None, **extra)- Updates or creates DNS records
- Should handle both creation and update logic, if supported by the provider
- Must return
Trueon success,Falseon failure with appropriate error logging - Should never raise exceptions for API failures
Optional:
_validate()- Custom authentication validation (has default implementation)
Available Methods:
_http(method, url, ...)- HTTP/HTTPS requests with automatic error handling_mask_sensitive_data(data)- Log-safe data masking for security (supports URL-encoded data)
BaseProvider (Full CRUD DNS Provider - Recommended for Most Providers)
Purpose: For DNS providers supporting complete DNS record management with query capabilities.
Must Implement:
_query_zone_id(domain)- Retrieves zone ID for a domain by calling domain info or list domains/zones API_query_record(zone_id, subdomain, main_domain, record_type, line, extra)- Finds existing DNS record by calling list records or query record API_create_record(zone_id, subdomain, main_domain, value, record_type, ttl, line, extra)- Creates new DNS record by calling create record API_update_record(zone_id, old_record, value, record_type, ttl, line, extra)- Updates existing DNS record by calling update record API
Recommended Practices:
- Implement a
_request()method for signed/authenticated HTTP requests:- Should raise
ExceptionorRuntimeErroron blocking errors for fast failure - Should return
Noneor appropriate default on recoverable errors (e.g., NotFound)
- Should raise
- Use
self.loggerfor consistent logging throughout the provider
Inherited Methods:
_http()- HTTP requests with authentication error handling (raises RuntimeError on 401/403)set_record()- Automatic record management (orchestrates the above abstract methods)
Code Quality Standards
Type Hints and Annotations
# Use complete type hints for all functions
def update_record(self, record_id, value, ttl=None):
# type: (str, str, int | None) -> bool
"""Update DNS record with new value."""
pass
Logging Best Practices
# Use structured logging with appropriate levels and consistent formatting
self.logger.info("Updating record: %s => %s", domain, value)
self.logger.debug("API response: %s", response_data)
self.logger.warning("Record not found: %s", domain)
self.logger.error("API call failed: %s", error)
self.logger.critical("Authentication invalid: %s", auth_error)
# Always mask sensitive data in logs to prevent credential exposure
self.logger.info("Request URL: %s", self._mask_sensitive_data(url))
Documentation Standards
Documentation Guidelines
- User Documentation (
doc/): End-user guides, CLI usage, and deployment instructions - Developer Documentation (
doc/dev/): API guides, architecture documentation, and contribution guidelines - Code Documentation: Inline docstrings and comments within source files
- Configuration Documentation: JSON schemas with examples and validation rules
Docstring Format
def create_record(self, zone_id, name, value, record_type="A"):
# type: (str, str, str, str) -> bool
"""
Create a new DNS record in the specified zone.
Args:
zone_id (str): DNS zone identifier
name (str): Record name (subdomain)
value (str): Record value (IP address, etc.)
record_type (str): Record type (A, AAAA, CNAME, etc.)
Returns:
bool: True if creation successful, False otherwise
Raises:
RuntimeError: When authentication fails (401/403 errors)
ValueError: When required parameters are invalid
"""
pass
Inline Comments
# Explain complex business logic with clear, concise comments
# Attempt to resolve zone automatically by walking up the domain hierarchy
domain_parts = domain.split(".")
for i in range(2, len(domain_parts) + 1):
candidate_zone = ".".join(domain_parts[-i:])
zone_id = self._query_zone_id(candidate_zone)
if zone_id:
break
Testing Guidelines
All tests must be placed in the tests/ directory, with a shared base test class for common functionality.
Test Structure
# tests/test_provider_example.py
from base_test import BaseProviderTestCase, MagicMock, patch
class TestExampleProvider(BaseProviderTestCase):
def setUp(self):
super(TestExampleProvider, self).setUp()
self.provider = ExampleProvider(self.id, self.token)
@patch("ddns.provider.example._http")
def test_create_record_success(self, mock_http):
# Arrange
mock_http.return_value = {"id": "record123", "status": "success"}
# Act
result = self.provider._create_record("zone1", "test", "example.com", "1.2.3.4", "A", None, None, {})
# Assert
self.assertTrue(result)
mock_http.assert_called_once()
Performance and Security
HTTP Client Usage
# Always use the base class _http method for consistent behavior
response = self._http("POST", "/api/records",
body={"name": name, "value": value},
headers={"Content-Type": "application/json"})
# Handle authentication errors appropriately
# Note: 401/403 errors will automatically raise RuntimeError
if response is None:
self.logger.error("API request failed")
return False
Development Workflow
Code Validation
- Install Ruff: Before making changes, install ruff linter/formatter:
pip3 install ruff - Linting: Run
ruff check --fix --unsafe-fixes .before each commit to fix linting issues - Formatting: Run
ruff format .before each commit to format code consistently - Type Checking: Ensure Pylance compatibility
- Testing: Run tests before committing changes
- Documentation: Update relevant documentation when making changes
Documentation Maintenance
- API Changes: Update docstrings and
doc/dev/provider.mdfor provider interface changes - Configuration Changes: Update JSON schemas in
schema/directory - User-Facing Changes: Update CLI documentation in
doc/config/cli.md - Examples: Keep code examples in documentation synchronized with actual implementation
Terminal Usage Guidelines for Copilot Agent
- Minimize Usage: Use terminal commands sparingly to reduce complexity
- Windows Compatibility: Avoid
&&and||operators that may not work on all shells - No Directory Changes: Use absolute paths instead of
cdcommands for reliability - Avoid Manual Compilation: Use editor extensions for syntax checking instead of
python -c
Common Anti-Patterns to Avoid
# DON'T: Use f-strings (incompatible with Python 2.7)
error_msg = f"Failed to update {domain}" # Not supported in Python 2.7
# DO: Use .format() or % formatting for compatibility
error_msg = "Failed to update {}".format(domain)
error_msg = "Failed to update %s" % domain
error_msg = "Failed to update "+ domain # Concatenation is also acceptable
# DON'T: Use broad type ignores that hide potential issues
result = api_call() # type: ignore
# DO: Use specific ignores only when absolutely necessary
result = api_call() # type: ignore[attr-defined]
# DON'T: Return inconsistent types that confuse callers
def get_record(self, name):
if found:
return {"id": "123", "name": name}
return False # Inconsistent return type
# DO: Return consistent types with clear semantics
def get_record(self, name):
if found:
return {"id": "123", "name": name}
return None # Consistent with Optional[dict]
# DON'T: Use bare except clauses that hide errors
try:
result = api_call()
except:
return None
# DO: Catch specific exceptions and handle appropriately
try:
result = api_call()
except (ValueError, TypeError) as e:
self.logger.error("API call failed: %s", e)
return None
Creating a New Provider
Development Steps
To create a new DNS provider, follow these steps:
- Create a new file in the
ddns/provider/directory, e.g.,myprovider.py. - Implement the provider class inheriting from
BaseProviderorSimpleProvideras appropriate based on the API capabilities.- For the BaseProvider interface, implement these required methods:
_query_zone_id(domain)_query_record(zone_id, subdomain, main_domain, record_type, line, extra)_create_record(zone_id, subdomain, main_domain, value, record_type, ttl, line, extra)_update_record(zone_id, old_record, value, record_type, ttl, line, extra)
- For the SimpleProvider interface, implement this required method:
set_record(domain, value, record_type="A", ttl=None, line=None, **extra)
- For the BaseProvider interface, implement these required methods:
- Implement the
_request()method for authenticated API calls. - Add the provider to the
ddns/__init__.pyfile to make it available in the main package. - Add unit tests in the
tests/directory to cover all methods and edge cases. - Update schema in
schema/v4.0.jsonif the provider requires new configuration options. - Create documentation in
doc/providers/myprovider.mdfor configuration and usage instructions. - Run all tests to ensure compatibility and correctness.
For detailed implementation guidance, refer to the provider development guide in doc/dev/provider.md.
Conventional Commits
commit message and pull request title format should follow the Conventional Commits specification (<type>(<scope>): <description>):
feat(provider.myprovider): add myprovider support
fix(util.http): correct authentication logic
docs(provider.myprovider): update myprovider configuration guide