&& and || operators that may not work on all shellscd commands; you are always in the project rootpython3 instead of python, and this is the only command you should usesix library patternsasync/await syntaxddns/ # 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
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)
True on success, False on failure with appropriate error loggingOptional:
_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)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 APIRecommended Practices:
_request() method for signed/authenticated HTTP requests:
Exception or RuntimeError on blocking errors for fast failureNone or appropriate default on recoverable errors (e.g., NotFound)self.logger for consistent logging throughout the providerInherited Methods:
_http() - HTTP requests with authentication error handling (raises RuntimeError on 401/403)set_record() - Automatic record management (orchestrates the above abstract methods)# 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
# 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))
doc/): End-user guides, CLI usage, and deployment instructionsdoc/dev/): API guides, architecture documentation, and contribution guidelinesdef 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
# 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
All tests must be placed in the tests/ directory, with a shared base test class for common functionality.
# 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()
# 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
doc/dev/provider.md for provider interface changesschema/ directorydoc/config/cli.md&& and || operators that may not work on all shellscd commands for reliabilitypython -c# 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
To create a new DNS provider, follow these steps:
ddns/provider/ directory, e.g., myprovider.py.BaseProvider or SimpleProvider as appropriate based on the API capabilities.
_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)set_record(domain, value, record_type="A", ttl=None, line=None, **extra)_request() method for authenticated API calls.ddns/__init__.py file to make it available in the main package.tests/ directory to cover all methods and edge cases.schema/v4.0.json if the provider requires new configuration options.doc/providers/myprovider.md for configuration and usage instructions.For detailed implementation guidance, refer to the provider development guide in doc/dev/provider.md.
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