Country Field
The openviper.contrib.countries package provides a lightweight,
zero-overhead CountryField for OpenViper models. It stores
ISO 3166-1 alpha-2
two-letter country codes in a CHAR(2) column.
All lookups operate on an in-memory frozenset; no database tables,
no ORM migrations for country data, and no external API calls are ever
made.
Overview
ISO 3166-1 alpha-2 — 249 territories built-in.
O(1) validation —
frozensetmembership test.LRU-cached helpers — choices and full registry cached after first access.
Strict input length guard — inputs longer than 10 characters are rejected before any lookup, preventing DoS via huge strings.
Uppercase normalisation — values are always stored and returned in uppercase.
OpenAPI enum —
CountryField.openapi_schema()returns a JSON Schema snippet with anenumof all valid codes.Serializer support —
to_representation()can return either the raw code or a full{"code", "name", "dial_code"}dict.
Installation
openviper.contrib.countries is part of the core distribution. No extra
packages are required. Simply import CountryField:
from openviper.contrib.countries import CountryField
Usage
Basic model field:
from openviper.db import Model
from openviper.contrib.countries import CountryField
class UserProfile(Model):
country = CountryField(null=True, db_index=True)
ORM filtering works as expected because the stored value is a plain uppercase string:
# Exact match
profiles = await UserProfile.objects.filter(country="GB").all()
# Nullable field — NULL rows
profiles = await UserProfile.objects.filter(country=None).all()
Field options
CountryField accepts all standard CharField
keyword arguments plus:
extra_countriesA tuple of
(code, name, dial_code)triples for project-specific regions not in the ISO standard.class Profile(Model): region = CountryField( extra_countries=( ("XA", "Atlantis", "+000"), ) )
strictWhen
True(default) the field only accepts exactly two ASCII letter codes. Set toFalseto relax this constraint for custom codes that use digits or other characters.
Settings
Add COUNTRY_FIELD to your project Settings subclass to customise
behaviour:
@dataclasses.dataclass(frozen=True)
class MySettings(Settings):
COUNTRY_FIELD: dict = dataclasses.field(
default_factory=lambda: {
"EXTRA_COUNTRIES": {
"XA": {"name": "Atlantis", "dial_code": "+000"},
},
"ENABLE_CACHE": True,
"STRICT": True,
}
)
Key |
Default |
Description |
|---|---|---|
EXTRA_COUNTRIES |
|
Custom codes not in the ISO dataset. |
ENABLE_CACHE |
|
Keep LRU caches warm (exposed for testing). |
STRICT |
|
Require exactly two ASCII letters. |
Serializer usage
Return the raw ISO code (default):
# Serializer output:
{"country": "GB"}
Return the full country object:
from openviper.contrib.countries import CountryField
class ProfileSerializer(ModelSerializer):
class Meta:
model = UserProfile
def to_representation(self, instance):
data = super().to_representation(instance)
field = self.model._meta.fields["country"]
data["country"] = field.to_representation(
instance.country, full=True
)
return data
# Serializer output:
{
"country": {
"code": "GB",
"name": "United Kingdom",
"dial_code": "+44"
}
}
OpenAPI example
When building OpenAPI schemas, call CountryField.openapi_schema() to
receive a ready-made JSON Schema fragment:
CountryField.openapi_schema()
# {
# "type": "string",
# "enum": ["AD", "AE", "AF", ..., "ZW"],
# "description": "ISO 3166-1 alpha-2 country code",
# "pattern": "^[A-Z]{2}$",
# "example": "GB"
# }
Utility helpers
The following helpers are exported directly from openviper.contrib.countries:
- validate_country(code, extra=(), strict=True) bool
Return
Trueif code is a valid alpha-2 country code.
- get_dial_code(code, extra=()) str | None
Return the international dialling prefix for code, or
None.
Performance
The full dataset is loaded once at first import and held in the process memory. There is no per-request overhead.
validate_country()performs a singlefrozenset.__contains__call: O(1) constant time.get_countries(),get_country(), andget_country_choices()are decorated withfunctools.lru_cacheso repeated calls with identical arguments return the cached result instantly.The
CHAR(2)column type and optionaldb_index=Trueensure fast equality filters andORDER BY countryqueries at the database level.
Security
Length guard — values longer than 10 characters are rejected before any lookup, preventing denial-of-service via pathological input.
Strict mode — the default
strict=Trueenforces the two-letter alphabetic format, blocking numeric codes, SQL injection strings, and other malformed inputs.Uppercase normalisation — all values are uppercased before both storage and validation, eliminating case-sensitivity edge cases.
No dynamic SQL — country validation never touches the database.
Testing
Unit tests live in tests/unit/countries/test_country_field.py.
Integration tests live in tests/integration/countries/test_country_integration.py.
Run them with:
pytest tests/unit/countries/ tests/integration/countries/ -v