Metadata-Version: 2.4
Name: whois-python
Version: 1.0.0
Summary: Simple and fast domain WHOIS lookup and availability checking, with no dependencies.
Author-email: MonoVM <dev@monovm.com>
Maintainer-email: MonoVM <dev@monovm.com>
License: MIT
Project-URL: Homepage, https://github.com/monovm/whois-python
Project-URL: Repository, https://github.com/monovm/whois-python
Project-URL: Issues, https://github.com/monovm/whois-python/issues
Project-URL: Changelog, https://github.com/monovm/whois-python/blob/main/CHANGELOG.md
Project-URL: PHP version, https://github.com/monovm/whois-php
Keywords: whois,rdap,domain,domain-availability,domain-checker,tld,dns
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Internet :: Name Service (DNS)
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Networking
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

# :zap: Simple and Fast Domain Whois Lookup in Python :zap:

[![PyPI](https://img.shields.io/pypi/v/whois-python.svg)](https://pypi.org/project/whois-python/)
[![Python versions](https://img.shields.io/pypi/pyversions/whois-python.svg)](https://pypi.org/project/whois-python/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

This Python package enables developers to retrieve domain registration information and check
domain availability over the WHOIS (port 43) and RDAP/HTTP protocols. It's a useful tool for web
developers and domain name registrars.

Python port of [monovm/whois-php](https://github.com/monovm/whois-php), which is based on the
WHMCS domain Whois class. **No third-party dependencies** — standard library only.

## :scroll: Installation

```bash
pip install whois-python
```

## :arrow_forward: Checker class

You can use this class to check the availability of one or multiple domains.

## :mortar_board: Usage/Examples

```python
from monovm_whois import Checker

# Single domain whois
result1 = Checker.whois("monovm.com")

# Single domain whois without specifying TLD
result2 = Checker.whois("monovm")

# Multiple domains whois
result3 = Checker.whois(["monovm", "google.com", "bing"])
```

- ### Response

A dict with domains as keys and status as values.

| Status        | Meaning                                                                    |
|---------------|----------------------------------------------------------------------------|
| `available`   | The registry says the domain is not registered                              |
| `unavailable` | The domain is registered                                                    |
| `premium`     | The registry flagged the name as premium/reserved                           |
| `invalid`     | Not a usable domain name, or no WHOIS server is known for the TLD           |
| `error`       | The lookup failed or the server declined to answer — retry, never a verdict |
| `unknown`     | The lookup returned nothing usable                                          |

`error` is deliberately not folded into `available`: a rate-limited or blocked server tells you
nothing about the domain.

```python
result1 = {"monovm.com": "unavailable"}

result2 = {
    "monovm.com": "unavailable",
    "monovm.net": "unavailable",
    "monovm.org": "unavailable",
    "monovm.info": "unavailable",
}

result3 = {
    "monovm.com": "unavailable",
    "monovm.net": "unavailable",
    "monovm.org": "unavailable",
    "monovm.info": "unavailable",
    "google.com": "unavailable",
    "bing.com": "unavailable",
    "bing.net": "unavailable",
    "bing.org": "unavailable",
    "bing.info": "unavailable",
}
```

A `premium` result means the registry withheld the record and flagged the name as reserved — for
example `nic.ir`-class names under IRNIC. A registered domain whose record the registry *did*
disclose is `unavailable`, not `premium`.

### :fire: popularTLDs configuration

When the TLD is not specified in the domain string (e.g. `monovm` instead of `monovm.com`), the
`Checker` class will automatically look up a list of popular TLDs for the entered name.

You can customize this list by passing an options dict as the second argument of `whois`.

```python
from monovm_whois import Checker

result = Checker.whois("monovm", {"popularTLDs": [".com", ".net", ".org", ".info"]})
```

`popular_tlds` is accepted as an alias, and a leading dot is optional (`'net'` == `'.net'`).

## :arrow_forward: WhoisHandler class

## :mortar_board: Usage/Examples

```python
from monovm_whois import WhoisHandler

whois_handler = WhoisHandler.whois("monovm.com")
```

#### Available methods:

**After initiating the handler you will have access to the following methods:**

| Method                     | Description                                                                                         |
|----------------------------|-----------------------------------------------------------------------------------------------------|
| `is_available()`           | Returns `True` if the domain is available for registration (uses enhanced detection)                 |
| `is_premium()`             | Returns `True` if the registry flagged the name as premium/reserved                                  |
| `is_valid()`               | Returns `True` if the domain can be looked up                                                        |
| `get_whois_message()`      | Returns the whois server message, including availability, validation or the domain whois information |
| `get_raw_whois_message()`  | Same message with the HTML escaping and `<br />` tags removed                                        |
| `get_tld()`                | Returns the top level domain of the entered domain as a string                                       |
| `get_sld()`                | Returns the second level domain of the entered domain as a string                                    |
| `get_availability_details()` | Returns detailed information about how availability was determined (debug method)                  |

Every method is also exposed under its PHP `camelCase` name (`isAvailable`, `getWhoisMessage`,
`getTld`, …) so code can be moved over from the PHP package unchanged.

:green_circle: `is_available()` reports the verdict the detection engine reached during the lookup.
The engine asks a chain of rules in order and takes the first conclusive answer:

1. **Unsupported or unwell server** — an IP-number registry banner, a busy server, a timeout.
2. **Explicit unavailability** — `Status: registered`, `Status: connect`, redemption and pending
   delete states, registry restriction notices; including keys padded with dots.
3. **Registration evidence** — enough record fields (`Registrar:`, `Name Server:`,
   `Creation Date:` …), or an RDAP record.
4. **Server declined** — rate limiting, a blocked client, port 43 retired in favour of RDAP.
5. **Premium marker** — the TLD definition's `premium` text, from the server definitions.
6. **Registry marker** — the TLD definition's `available` text.
7. **Availability keywords** — "no match", "not found", "no entries found", "is free" and 30 more,
   in several languages, ignoring comment and banner lines.
8. **No-match patterns** — regex forms of the above, plus a genuine RDAP 404.
9. **TLD-specific patterns** — registries phrase availability differently.
10. **Explicit status fields** — `Status: available`, `Registration status: available`.
11. **Default to registered** — with no positive evidence, the name is assumed taken.

The order matters more than the rules. Everything that could mean "this is not an answer" or "this
is a registration" is asked before anything that could mean "available", so availability is only
ever reported on positive evidence and is never the fallback. A reply that carries no verdict — a
rate-limit notice, a blocked client, an HTTP 403, a retired endpoint, an empty read — raises
`WhoisServerError` rather than being read as "free"; see
[Differences from the PHP package](#warning-differences-from-the-php-package).

```python
available = whois_handler.is_available()
```

:green_circle: `is_valid()` checks whether the entered domain name is valid and can be looked up.

```python
valid = whois_handler.is_valid()
```

:green_circle: `get_whois_message()` retrieves the WHOIS information of a domain. It returns a
string that includes the WHOIS server message, which may contain information about the
availability and validation of the domain, as well as its WHOIS information.

```python
message = whois_handler.get_whois_message()
```

:green_circle: `get_tld()` extracts the top level domain (TLD) of a given domain. For example, if
the domain name passed to the handler is `monovm.com`, the method returns `.com`. Similarly, if
the domain name is `monovm.co.uk`, the method returns `.co.uk`.

```python
tld = whois_handler.get_tld()
```

:green_circle: `get_sld()` returns the second level domain of the entered domain as a string. For
example, in the domain name `monovm.com`, the second level domain is `monovm`.

```python
sld = whois_handler.get_sld()
```

:green_circle: `get_availability_details()` provides detailed debugging information about how the
domain availability was determined. It returns a dict containing the result of each detection
method:

```python
details = whois_handler.get_availability_details()

# {
#     'original_library_result': False,
#     'contains_no_verdict_markers': False,
#     'contains_unsupported_tld_messages': False,
#     'contains_unavailability_indicators': False,
#     'contains_registration_indicators': False,
#     'contains_availability_keywords': True,
#     'is_response_too_short': False,
#     'contains_no_match_patterns': True,
#     'tld_specific_patterns': False,
#     'domain_status_indicators': False,
#     'final_availability': True,   # or 'unsupported_tld' / 'no_verdict'
#     'whois_message_length': 1234,
#     'whois_message_preview': 'No match for domain example123.com...',
# }
```

## :arrow_forward: Module-level shortcuts

```python
import monovm_whois

monovm_whois.whois("monovm.com")  # -> {'monovm.com': 'unavailable'}
monovm_whois.is_available("monovm.com")  # -> False
monovm_whois.lookup("monovm.com")  # -> WhoisHandler
```

## :arrow_forward: Command line

The package installs a `monovm-whois` command (also runnable as `python -m monovm_whois`):

```bash
$ monovm-whois monovm.com bing
monovm.com   unavailable
bing.com     unavailable
bing.net     unavailable
bing.org     available
bing.info    unavailable

$ monovm-whois monovm.com --json
{
  "monovm.com": "unavailable"
}

$ monovm-whois monovm.com --record      # print the full WHOIS record
$ monovm-whois monovm --tlds .com,.dev  # TLDs to try when none is given
```

## :gear: Configuration

Every entry point accepts the same transport options, either as keyword arguments or inside the
`Checker.whois` options dict:

| Option             | Default | Description                                        |
|--------------------|---------|----------------------------------------------------|
| `socket_timeout`   | `10.0`  | Connect/read timeout for port 43 lookups (seconds) |
| `http_timeout`     | `60.0`  | Total timeout for RDAP/HTTP lookups (seconds)      |
| `verify_ssl`       | `False` | Verify TLS certificates on RDAP/HTTP lookups       |
| `override_path`    | `None`  | Extra JSON file merged over the bundled server list |
| `definitions_path` | bundled | Replace the bundled server list entirely            |
| `unicode_query_tlds` | `{".de"}` | TLDs whose registry wants Unicode, not punycode, for IDNs |

```python
from monovm_whois import Checker, WhoisHandler

WhoisHandler.whois("monovm.com", socket_timeout=5, verify_ssl=True)
Checker.whois("monovm.com", {"socket_timeout": 5})
```

`verify_ssl` is off by default to match the PHP client: several registry RDAP endpoints still
serve incomplete certificate chains. Turn it on when you only query well-behaved registries.

### Adding or overriding WHOIS servers

Point `MONOVM_WHOIS_DEFINITIONS` at a JSON file, or pass `override_path`. Its entries are merged
on top of the bundled `dist.whois.json`, so you can add a TLD or replace an existing server:

```json
[
  {
    "extensions": ".example,.test",
    "uri": "socket://whois.example.test",
    "available": "No match for"
  }
]
```

| Field                  | Meaning                                                                 |
|------------------------|-------------------------------------------------------------------------|
| `extensions`           | Comma separated TLDs this entry serves                                   |
| `uri`                  | `socket://host[:port]` for WHOIS port 43, or an `https://…/domain/` RDAP base |
| `available`            | Text that appears when the domain is unregistered                        |
| `premium`              | Optional: text that marks a premium/reserved name                        |
| `available_when_empty` | Optional: `"true"` for registries that answer an unregistered name with nothing but a banner |

## :globe_with_meridians: Internationalised domains

IDNs work in either form, and each registry is queried the way it expects:

```python
Checker.whois("bücher.com")  # -> {'bücher.com': 'unavailable'}
Checker.whois("xn--bcher-kva.com")  # -> {'xn--bcher-kva.com': 'unavailable'}
Checker.whois("münchen.de")  # -> {'münchen.de': 'unavailable'}
```

Punycode is used on the wire because Verisign and most registries answer "No match" to a Unicode
query — which would look like availability. DENIC is the exception and gets the Unicode form; add
more with `unicode_query_tlds`. Result keys keep the form you passed in.

Input is normalised before use, so URLs, mixed case, subdomains and stray whitespace all work:

```python
Checker.whois("HTTPS://WWW.Example.COM/pricing?x=1")  # -> {'example.com': 'unavailable'}
```

## :globe_with_meridians: Whois server list
### Almost all TLDs are supported.

870+ extensions ship with the package, served over WHOIS port 43 or RDAP. Inspect them at runtime:

```python
from monovm_whois import Whois

Whois().supported_tlds()  # ['.abogado', '.ac', '.academy', ...]
Whois().can_lookup(".dev")  # True
```

## :building_construction: Architecture

The classes above are a facade. Underneath, each concern is a separate object, and
`LookupService` is the only thing that knows the whole sequence — find the definition, pick a
transport, choose the query form, fetch, classify, format. It owns none of the steps:

| Module | Responsibility | Extension point |
|--------|----------------|-----------------|
| [`definitions`](src/monovm_whois/definitions.py) | Where TLD server definitions come from | `DefinitionRepository` — JSON file, in-memory, or chained |
| [`resolver`](src/monovm_whois/resolver.py) | Which suffix a host name ends with | `TldResolver`, longest-suffix first |
| [`transport`](src/monovm_whois/transport/) | How to talk to a registry | `Transport` per protocol, chosen by `TransportFactory` on URI scheme |
| [`detection`](src/monovm_whois/detection/) | What the reply means | An ordered chain of `DetectionRule` objects walked by `DetectionEngine` |
| [`formatting`](src/monovm_whois/formatting.py) | How the record is rendered | `RecordFormatter` — HTML or plain text |
| [`service`](src/monovm_whois/service.py) | The sequence | `LookupService`, everything injected |

So customising is assembly, not subclassing:

```python
from monovm_whois import Checker, LookupService, TransportFactory
from monovm_whois.definitions import InMemoryDefinitionRepository

service = LookupService(
    repository=InMemoryDefinitionRepository(
        {".internal": {"uri": "socket://whois.corp.example", "available": "Domain not found"}}
    ),
    transports=TransportFactory.default(socket_timeout=2),
)
Checker.whois("anything.internal", {"service": service})
```

Adding a protocol is a registration rather than an edit to the lookup path:

```python
from monovm_whois import Transport, TransportFactory
from monovm_whois.transport import RawResponse


class MyProtocolTransport(Transport):
    schemes = ("myproto",)

    def fetch(self, query, endpoint):
        return RawResponse(my_client.lookup(query), endpoint=endpoint)


factory = TransportFactory.default().register(MyProtocolTransport())
```

Adding a detection signal is a new rule, inserted where its priority belongs:

```python
from monovm_whois import DetectionEngine, DetectionRule, Verdict


class MyRegistryRule(DetectionRule):
    name = "my registry"

    def evaluate(self, context):
        if "SPECIAL-RESERVED" in context.response.significant:
            return Verdict.REGISTERED
        return None  # defer to the rest of the chain


engine = DetectionEngine((MyRegistryRule(),) + DetectionEngine.default_rules())
engine.explain("...", ".com").rule_name  # which rule decided, and why
```

The chain's **order is the safety policy**. Everything that could mean "this is not an answer" or
"this is a registration" is asked before anything that could mean "available", because the one
unacceptable mistake is calling a registered domain free:

```
unsupported/unwell server → explicit unavailability → registration evidence → server declined
  → premium marker → registry marker → availability keyword → no match
  → tld-specific availability → status field → default to registered
```

Note where the two *marker* rules sit. A definition's `available` marker is a plain substring, and
some are a single word (`.it` uses `AVAILABLE`), so a marker is treated as a hint and asked only
once a real record has been ruled out. Availability is never the fallback.

## :warning: Differences from the PHP package

The detection logic is a port of the PHP original, and 48 of the 60 recorded test responses
classify identically. The differences below are deliberate.

### Never report a registered domain as free

That is the one mistake this library must not make, and the PHP original makes it whenever a
server replies with something other than a record — because "fewer than two registration fields"
was treated as evidence of availability. Every such reply (a rate-limit notice, a blocked client,
an HTTP 403, a retired endpoint, a legal preamble, a truncated read) has no registration fields
either. This port removes that inference and instead:

- raises `WhoisServerError` when the server declines to answer — rate limiting
  (`request limit exceeded`, `Maximum query rate reached`), a blocked client
  (`Requests of this client are not permitted`), or port 43 retired in favour of RDAP;
- raises `WhoisServerError` on HTTP 401/403/405/406/429 and 5xx, while still treating an RDAP
  **404** as the "no such domain" answer it is;
- raises `UnsupportedTldError` when the reply is an IP-number registry banner (RIPE, APNIC, ARIN,
  LACNIC, AFRINIC) — the TLD is mapped to the wrong server, and those answer
  `%ERROR:101: no entries found` to every domain query;
- returns an error for an empty or whitespace-only reply instead of calling it available;
- recognises records whose keys are padded with dots (`status.............: Registered`), which
  Traficom (.fi) and NIC Monaco use and a plain `status:` match misses;
- reports **premium/reserved** names as unavailable. PHP re-analyses the
  `"No WHOIS information available."` placeholder there and returns `true`; here `is_available()`
  is `False` and `is_premium()` is `True`;
- reports a junk or empty domain string as invalid rather than available.

Registries that genuinely answer an unregistered name with nothing but a banner opt in per TLD
with `available_when_empty`, so the inference applies only where it is the documented behaviour
and never overrides a refusal.

### Over-broad patterns, anchored

The PHP tables match several bare words anywhere in a reply. Registry replies are not just
records — they also carry legal banners, field *names* and prose — so each of these reported a
registered domain as free, or an unregistered one as taken:

| Pattern | Where it went wrong | Now |
|---------|--------------------|-----|
| `available` | Bare, in **81** per-TLD lists. Matches "Notice, available at https://…", and Traficom prints `available.........: <date>` on *registered* `.fi` domains | Anchored patterns requiring an assertion about the domain; per-TLD patterns match data lines, not the banner |
| `404` | Matched a registrant's street number, a phone number, a registry object id | Anchored to the RDAP and HTTP shapes that mean it; `.ec` and `.shop` now match `"errorCode"` |
| `registered` | Bare, in the `.uk` list — and Nominet's free reply reads "This domain name has **not** been registered", so every free `.uk` domain read as taken | The affirmative forms: `Registered on:` and the explicit sentence |
| `not exist` | Matched prose such as "a cached copy may not exist" | Requires a subject: "the domain/object/name does not exist" |
| `free` | Matched a registry's "free FAQ" footer link | `is free`, `status: free` |
| `status:\tavailable` | Transcribed into a Python *raw* string, so the tab became a literal backslash-`t` and never matched | A real tab, which also restores agreement with PHP |
| `---not found` | Listed in **both** the availability and unavailability tables, unavailability checked first, so "Not found: free.sx" came back registered — and it is the very marker `.io` and `.sg` rely on | Availability table only |

Two further cases are not about patterns but about precedence:

- A reserved name could read as free, because IRNIC announces one as "This domain is only
  **available for registration** under certain conditions". Premium is now a verdict asked before
  every availability rule.
- Identity Digital's `.io` terms of use say "**If** too many queries are received…" — a
  conditional about policy, which turned every free `.io` lookup into a rate-limit error. Refusal
  detection now skips conditional sentences.

### Corrected WHOIS servers

Each of these answered "not found" for every domain, so every domain under them looked free.
Replacements were verified against the live registries in both directions:

| TLDs | Was | Now |
|------|-----|-----|
| `.es .com.es .nom.es .gob.es .edu.es` | `whois.crsnic.net` (Verisign does not serve .es) | `whois.nic.es` — Red.es retired its public port 43, so lookups now error instead of lying |
| `.online`, `.site` | `whois.centralnic.com` | `whois.nic.online`, `whois.nic.site` |
| `.li` | `whois.nic.li` (refuses most clients) | `whois.nic.ch:4343` (SWITCH serves .li there) |
| `.shop` | `whois.nic.shop` (port 43 retired 2026-05-01) | `https://rdap.gmoregistry.net/rdap/domain/` |
| `.ad` | `whois.ripe.net` | `whois.nic.ad` |
| `.asso.mc`, `.tm.mc` | `whois.ripe.net` | `whois.nic.mc` |
| `.com.tw .net.tw .org.tw` | `whois.twnic.net` (now answers from APNIC) | `whois.twnic.net.tw` |
| `.ru.com` | `whois.verisign-grs.com` | `whois.centralnic.com` |
| `.com.ru .net.ru .org.ru .pp.ru` | `whois.ripn.net` (private zones are absent from it) | `whois.nic.ru` |
| `.gt .com.gt .net.gt .org.gt .ind.gt .edu.gt .gob.gt .mil.gt` | an HTML page that is now a JavaScript app | removed — IANA publishes no WHOIS server, so these report `invalid` |

### Other differences

- Input is normalised: case, whitespace, a trailing root dot, URLs, ports and subdomains
  (`https://www.example.co.uk/x` → `example.co.uk`). The TLD is matched longest-suffix first.
- IDNs are converted to punycode for the query, except for registries in `unicode_query_tlds`.
- `Whois.lookup()` returns `None` (instead of `false`) when no server is known for the TLD.
- Errors raise typed exceptions — `WhoisError` and its subclasses `UnsupportedTldError`,
  `WhoisServerError`, `WhoisConnectionError`, `DefinitionsError`, `InvalidDomainError`,
  `EmptyResponseError` — rather than a generic exception.
- Bad input raises `TypeError`/`ValueError` with a message instead of failing silently: a
  non-string domain, an unknown option key, an empty `popularTLDs`.
- Duplicate domains in one `Checker.whois()` call are looked up once.
- Parsed server definitions are cached, so bulk checks don't re-read the JSON per domain.
- Detection patterns are compiled once at import rather than per call.
- Timeouts, TLS verification, the server list and the IDN query form are configurable.
- `get_availability_details()` gains `contains_no_verdict_markers`, and `final_availability` can
  be `"no_verdict"`.
- `get_availability_details()['whois_message_length']` counts characters; PHP's `strlen` counts
  bytes, so the two differ for responses containing non-ASCII text.

### Other differences in this version

- `Checker` composes a `Whois` rather than extending it. It could never substitute for one — its
  constructor requires a domain — so the inheritance only obscured that. The client is
  `checker.client`.
- `WhoisHandler.is_available()` reports the verdict reached during the lookup instead of re-running
  detection over the display message, and no longer flips `is_valid()` as a side effect.
- A premium name that came with a full record is reported `unavailable`, not `premium`: the
  registry showed a registration, so the name is taken. `premium` is for the case where the
  registry withholds the record.
- `monovm_whois.utils` was split into `monovm_whois.names` (domain semantics) and
  `monovm_whois.text` (presentation helpers). The re-exports on `monovm_whois` are unchanged.

### How this is kept honest

Three corpora, all replayed offline:

- **`tests/fixtures/live_registry.json`** — 26 replies captured verbatim from 15 registries, one
  registered and one unregistered name per TLD, run through the whole lookup path including each
  TLD's own definition marker. Every pattern fix above was caught or confirmed here; two of them
  were found *only* because a real registry does something no synthetic case predicted.
- **`tests/fixtures/php_parity.json`** — 60 responses, each stored with **both** the PHP verdict
  and per-method flags and the reviewed verdict expected here. Any drift in either implementation
  fails the build; every divergence carries a recorded direction and reason, and every individual
  detection flag that differs from PHP's carries its own.
- **`tests/test_pattern_regressions.py`** — one test per defect above, plus assertions that no
  pattern table may reintroduce a bare dangerous word.

1350+ offline tests run at 100% line and branch coverage with no network access. The `network`-marked
tests additionally query live registries; those pace themselves and **skip** rather than fail when a
registry refuses to answer, since a throttled socket says nothing about this code.

## :test_tube: Development

```bash
git clone https://github.com/monovm/whois-python
cd whois-python
pip install -e ".[dev]"

pytest                          # offline only, 100% coverage
pytest --cov --cov-report=term-missing
RUN_NETWORK_TESTS=1 pytest      # also query real registries (throttling is skipped, not failed)
ruff check . && ruff format --check . && mypy
```

## :globe_with_meridians: Contributing
If you want to add support for a new TLD, extend functionality or correct a bug, feel free to
create a new pull request at the GitHub repository.

## :balance_scale: License

[MIT](https://choosealicense.com/licenses/mit/)

## :computer: Support

For support, email dev@monovm.com.

[MonoVM.com](https://monovm.com)

![Logo](https://monovm.com/site-assets/images/logo-monovm.svg)
