uaregistry 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uaregistry-1.0.0/LICENSE +21 -0
- uaregistry-1.0.0/PKG-INFO +235 -0
- uaregistry-1.0.0/README.md +209 -0
- uaregistry-1.0.0/pyproject.toml +35 -0
- uaregistry-1.0.0/setup.cfg +4 -0
- uaregistry-1.0.0/uaregistry/__init__.py +53 -0
- uaregistry-1.0.0/uaregistry/client.py +257 -0
- uaregistry-1.0.0/uaregistry/commands.py +406 -0
- uaregistry-1.0.0/uaregistry/config.py +49 -0
- uaregistry-1.0.0/uaregistry/exceptions.py +40 -0
- uaregistry-1.0.0/uaregistry/frame.py +93 -0
- uaregistry-1.0.0/uaregistry/namespaces.py +25 -0
- uaregistry-1.0.0/uaregistry/response.py +243 -0
- uaregistry-1.0.0/uaregistry/result_code.py +53 -0
- uaregistry-1.0.0/uaregistry/transport.py +123 -0
- uaregistry-1.0.0/uaregistry.egg-info/PKG-INFO +235 -0
- uaregistry-1.0.0/uaregistry.egg-info/SOURCES.txt +17 -0
- uaregistry-1.0.0/uaregistry.egg-info/dependency_links.txt +1 -0
- uaregistry-1.0.0/uaregistry.egg-info/top_level.txt +1 -0
uaregistry-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 UARegistry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uaregistry
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Dependency-free Python client for the UARegistry EPP service (RFC 5730-5734 over TLS).
|
|
5
|
+
Author: UARegistry
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/uaregistry/python-sdk
|
|
8
|
+
Project-URL: Repository, https://github.com/uaregistry/python-sdk
|
|
9
|
+
Project-URL: Issues, https://github.com/uaregistry/python-sdk/issues
|
|
10
|
+
Keywords: epp,registry,domains,rfc5730,uaregistry
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Internet :: Name Service (DNS)
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# UARegistry EPP SDK (Python)
|
|
28
|
+
|
|
29
|
+
A small, **dependency-free** Python client for the **UARegistry** EPP service — standard
|
|
30
|
+
**RFC 5730–5734** EPP over **TLS on port 700**. It speaks the wire protocol directly
|
|
31
|
+
(no framework, no server-side code), so you can drop it into any Python 3.8+ project.
|
|
32
|
+
Every command frame is standard, schema-valid EPP.
|
|
33
|
+
|
|
34
|
+
- TLS transport with correct RFC 5734 framing (4-byte length prefix, UTF-8 byte-safe).
|
|
35
|
+
- Session: `connect` / `login` / `logout`, with the login services taken from the server
|
|
36
|
+
greeting automatically (never rejected for an unsupported service).
|
|
37
|
+
- Full object commands: **domain**, **contact**, **host** (check / info / create / update /
|
|
38
|
+
delete / transfer / renew), plus **poll** and **balance**.
|
|
39
|
+
- Extensions: **secDNS** (RFC 5910), **RGP restore** (RFC 3915), and the UARegistry native
|
|
40
|
+
**.ua trademark licence**.
|
|
41
|
+
- Clean `Response` objects (result code, message, availability map, value getters) and typed
|
|
42
|
+
exceptions.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install git+https://github.com/uaregistry/python-sdk
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or pin a released tag:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install "uaregistry @ git+https://github.com/uaregistry/python-sdk@v1.0.0"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
No packaging at all? Copy the `uaregistry/` package folder next to your code and
|
|
57
|
+
`import uaregistry`. The SDK requires only the Python standard library
|
|
58
|
+
(`ssl`, `socket`, `xml.etree`).
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from uaregistry import Client, Config
|
|
64
|
+
from uaregistry.exceptions import EppException
|
|
65
|
+
|
|
66
|
+
client = Client(Config(
|
|
67
|
+
host="uaregistry.com",
|
|
68
|
+
clid="UAR0001",
|
|
69
|
+
password="your-secret",
|
|
70
|
+
port=700, # default; override only if the endpoint moves
|
|
71
|
+
lang="uk", # localized result messages: en | uk | ua | ru
|
|
72
|
+
# ca_file="/path/to/ca.pem", # for a private-CA / self-signed endpoint
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
client.connect() # TLS + read <greeting>
|
|
77
|
+
client.login()
|
|
78
|
+
|
|
79
|
+
avail = client.domain.check(["example.com.ua"]).availability()
|
|
80
|
+
# => {"example.com.ua": True}
|
|
81
|
+
|
|
82
|
+
info = client.domain.info("example.com.ua")
|
|
83
|
+
print(info.value("exDate"))
|
|
84
|
+
|
|
85
|
+
client.logout()
|
|
86
|
+
except EppException as exc:
|
|
87
|
+
print("EPP error:", exc)
|
|
88
|
+
finally:
|
|
89
|
+
client.disconnect()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`Client` is also a context manager (`with Client(cfg) as client: ...`) that disconnects on exit.
|
|
93
|
+
|
|
94
|
+
## TLS notes
|
|
95
|
+
|
|
96
|
+
| Scenario | Config |
|
|
97
|
+
|---|---|
|
|
98
|
+
| Public, browser-trusted cert | defaults (`verify_peer=True`, `verify_peer_name=True`) |
|
|
99
|
+
| Private-CA / self-signed endpoint | set `ca_file` to the CA `.pem` |
|
|
100
|
+
| Hostname mismatch (dev) | `verify_peer_name=False` |
|
|
101
|
+
| Mutual-TLS endpoint | `client_cert` + `client_key` (+ `client_key_passphrase` if the key is encrypted) |
|
|
102
|
+
|
|
103
|
+
The public endpoint on `uaregistry.com:700` is strict RFC EPP and needs **no client
|
|
104
|
+
certificate** — auth is clID + password (over TLS) with an IP allowlist.
|
|
105
|
+
|
|
106
|
+
## Commands
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# Session
|
|
110
|
+
client.connect(); client.login(); client.logout(); client.disconnect()
|
|
111
|
+
client.login("new-password") # rotate the EPP password during login
|
|
112
|
+
client.hello() # re-read the greeting / keep-alive
|
|
113
|
+
|
|
114
|
+
# Domain
|
|
115
|
+
client.domain.check(["a.com.ua", "b.com.ua"])
|
|
116
|
+
client.domain.info("a.com.ua", "pw")
|
|
117
|
+
client.domain.create("a.com.ua",
|
|
118
|
+
years=1, registrant="C1", contacts={"admin": "C1", "tech": "C2"},
|
|
119
|
+
nameservers=["ns1.x.ua", "ns2.x.ua"], auth_info="pw",
|
|
120
|
+
license="TM-123", # second-level .ua only
|
|
121
|
+
sec_dns={"ds_data": [{"key_tag": 12345, "alg": 8, "digest_type": 2, "digest": "ABCD..."}]})
|
|
122
|
+
client.domain.update("a.com.ua",
|
|
123
|
+
add={"ns": ["ns3.x.ua"], "statuses": ["clientHold"]},
|
|
124
|
+
rem={"statuses": ["clientHold"]},
|
|
125
|
+
chg={"registrant": "C9", "auth_info": "newpw"},
|
|
126
|
+
# DNSSEC (RFC 5910): sec_dns={"add": {"ds_data": [...]}, "rem_all": True, "max_sig_life": 1209600}
|
|
127
|
+
)
|
|
128
|
+
client.domain.renew("a.com.ua", "2027-01-15", 1)
|
|
129
|
+
client.domain.restore("a.com.ua") # RGP restore (op="request")
|
|
130
|
+
client.domain.delete("a.com.ua")
|
|
131
|
+
client.domain.transfer("request", "a.com.ua", "pw", 1)
|
|
132
|
+
|
|
133
|
+
# Contact
|
|
134
|
+
client.contact.check(["c1"])
|
|
135
|
+
client.contact.info("c1", "pw")
|
|
136
|
+
client.contact.create("c1", name="ACME", city="Kyiv", cc="UA", email="a@b.ua", auth_info="pw",
|
|
137
|
+
# postal_infos=[{"type": "int", ...}, {"type": "loc", ...}], # int + localized
|
|
138
|
+
# disclose={"flag": False, "addr": ["int"], "voice": True}, # RFC 5733 privacy
|
|
139
|
+
)
|
|
140
|
+
client.contact.update("c1", chg={"email": "new@b.ua"},
|
|
141
|
+
add_statuses=["clientUpdateProhibited"])
|
|
142
|
+
client.contact.delete("c1")
|
|
143
|
+
client.contact.transfer("request", "c1", "pw")
|
|
144
|
+
|
|
145
|
+
# Host
|
|
146
|
+
client.host.check(["ns1.x.ua"])
|
|
147
|
+
client.host.info("ns1.x.ua")
|
|
148
|
+
client.host.create("ns1.x.ua", ["203.0.113.10", "2001:db8::1"]) # v4/v6 auto-detected
|
|
149
|
+
client.host.update("ns1.x.ua", add_addresses=["203.0.113.11"])
|
|
150
|
+
client.host.delete("ns1.x.ua")
|
|
151
|
+
|
|
152
|
+
# Poll & balance
|
|
153
|
+
msg = client.poll.request() # 1301 with a message, 1300 when empty
|
|
154
|
+
if msg.message_id() is not None: # message_count() = how many remain
|
|
155
|
+
client.poll.ack(msg.message_id())
|
|
156
|
+
b = client.balance().balance() # {"creditLimit": ..., "balance": ..., "availableCredit": ...}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Responses
|
|
160
|
+
|
|
161
|
+
Every command returns a `Response`:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
r.code() # int EPP result code (1000, 1001, 2303, ...)
|
|
165
|
+
r.is_success() # True for 1xxx
|
|
166
|
+
r.is_pending() # True for 1001 (registry resolves via a poll message)
|
|
167
|
+
r.message() # human-readable <msg>
|
|
168
|
+
r.message_lang() # "en" | "uk" | "ua" | "ru"
|
|
169
|
+
r.availability() # {name: bool} for *:check
|
|
170
|
+
r.statuses() # ["ok"] or ["clientHold", ...]
|
|
171
|
+
r.value("exDate") # first element with that local name
|
|
172
|
+
r.values("ns") # all elements with that local name
|
|
173
|
+
r.balance() # {"creditLimit": ..., "balance": ..., "availableCredit": ...} or None
|
|
174
|
+
r.prices() # {"renewal": {"value": ..., "currency": "UAH"}, ...}
|
|
175
|
+
r.license() # .ua trademark/licence number, or None
|
|
176
|
+
r.rgp_status() # ["redemptionPeriod"], ...
|
|
177
|
+
r.transfer_status() # "pending" | "serverApproved" | ... or None
|
|
178
|
+
r.ds_records() # [{"keyTag":..,"alg":..,"digestType":..,"digest":..}, ...]
|
|
179
|
+
r.key_records() # [{"flags":..,"protocol":..,"alg":..,"pubKey":..}, ...]
|
|
180
|
+
r.is_signed() # bool: any DNSSEC data present
|
|
181
|
+
r.message_id() # poll: id to pass to poll().ack(); message_count() = queue size
|
|
182
|
+
r.error_reasons() # extra <extValue><reason> text on a failed command
|
|
183
|
+
r.sv_trid() # server transaction id
|
|
184
|
+
r.raw # the raw XML
|
|
185
|
+
r.root # the parsed ElementTree root, for anything bespoke
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Error handling
|
|
189
|
+
|
|
190
|
+
By default any EPP error code (>= 2000) raises `CommandException` (with `.epp_code` and
|
|
191
|
+
`.response`). Login failures raise `AuthenticationException`; transport problems raise
|
|
192
|
+
`ConnectionException`. All extend `EppException`.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from uaregistry.exceptions import CommandException
|
|
196
|
+
from uaregistry import ResultCode
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
client.domain.create("taken.com.ua", years=1, registrant="C1",
|
|
200
|
+
contacts={"admin": "C1", "tech": "C2"}, nameservers=["ns1.x.ua"])
|
|
201
|
+
except CommandException as exc:
|
|
202
|
+
if exc.epp_code == ResultCode.OBJECT_EXISTS: # 2302
|
|
203
|
+
...
|
|
204
|
+
|
|
205
|
+
# Prefer branching on codes yourself?
|
|
206
|
+
client.throw_on_failure(False)
|
|
207
|
+
resp = client.domain.info("maybe.com.ua")
|
|
208
|
+
if resp.code() == ResultCode.OBJECT_DOES_NOT_EXIST:
|
|
209
|
+
...
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Custom frames
|
|
213
|
+
|
|
214
|
+
Anything the high-level API doesn't cover can be built with `Frame` and sent raw:
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
from uaregistry import Frame, Namespaces
|
|
218
|
+
|
|
219
|
+
frame = Frame.command("my-trid-1")
|
|
220
|
+
check = frame.ns(frame.verb("check"), Namespaces.DOMAIN, "domain:check")
|
|
221
|
+
frame.ns(check, Namespaces.DOMAIN, "domain:name", "x.com.ua")
|
|
222
|
+
resp = client.request(frame) # or client.request(raw_xml_string)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Testing
|
|
226
|
+
|
|
227
|
+
A no-dependency offline self-test (frame building + response parsing, no server, no network):
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
python tests/offline_test.py
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# UARegistry EPP SDK (Python)
|
|
2
|
+
|
|
3
|
+
A small, **dependency-free** Python client for the **UARegistry** EPP service — standard
|
|
4
|
+
**RFC 5730–5734** EPP over **TLS on port 700**. It speaks the wire protocol directly
|
|
5
|
+
(no framework, no server-side code), so you can drop it into any Python 3.8+ project.
|
|
6
|
+
Every command frame is standard, schema-valid EPP.
|
|
7
|
+
|
|
8
|
+
- TLS transport with correct RFC 5734 framing (4-byte length prefix, UTF-8 byte-safe).
|
|
9
|
+
- Session: `connect` / `login` / `logout`, with the login services taken from the server
|
|
10
|
+
greeting automatically (never rejected for an unsupported service).
|
|
11
|
+
- Full object commands: **domain**, **contact**, **host** (check / info / create / update /
|
|
12
|
+
delete / transfer / renew), plus **poll** and **balance**.
|
|
13
|
+
- Extensions: **secDNS** (RFC 5910), **RGP restore** (RFC 3915), and the UARegistry native
|
|
14
|
+
**.ua trademark licence**.
|
|
15
|
+
- Clean `Response` objects (result code, message, availability map, value getters) and typed
|
|
16
|
+
exceptions.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install git+https://github.com/uaregistry/python-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or pin a released tag:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install "uaregistry @ git+https://github.com/uaregistry/python-sdk@v1.0.0"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
No packaging at all? Copy the `uaregistry/` package folder next to your code and
|
|
31
|
+
`import uaregistry`. The SDK requires only the Python standard library
|
|
32
|
+
(`ssl`, `socket`, `xml.etree`).
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from uaregistry import Client, Config
|
|
38
|
+
from uaregistry.exceptions import EppException
|
|
39
|
+
|
|
40
|
+
client = Client(Config(
|
|
41
|
+
host="uaregistry.com",
|
|
42
|
+
clid="UAR0001",
|
|
43
|
+
password="your-secret",
|
|
44
|
+
port=700, # default; override only if the endpoint moves
|
|
45
|
+
lang="uk", # localized result messages: en | uk | ua | ru
|
|
46
|
+
# ca_file="/path/to/ca.pem", # for a private-CA / self-signed endpoint
|
|
47
|
+
))
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
client.connect() # TLS + read <greeting>
|
|
51
|
+
client.login()
|
|
52
|
+
|
|
53
|
+
avail = client.domain.check(["example.com.ua"]).availability()
|
|
54
|
+
# => {"example.com.ua": True}
|
|
55
|
+
|
|
56
|
+
info = client.domain.info("example.com.ua")
|
|
57
|
+
print(info.value("exDate"))
|
|
58
|
+
|
|
59
|
+
client.logout()
|
|
60
|
+
except EppException as exc:
|
|
61
|
+
print("EPP error:", exc)
|
|
62
|
+
finally:
|
|
63
|
+
client.disconnect()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`Client` is also a context manager (`with Client(cfg) as client: ...`) that disconnects on exit.
|
|
67
|
+
|
|
68
|
+
## TLS notes
|
|
69
|
+
|
|
70
|
+
| Scenario | Config |
|
|
71
|
+
|---|---|
|
|
72
|
+
| Public, browser-trusted cert | defaults (`verify_peer=True`, `verify_peer_name=True`) |
|
|
73
|
+
| Private-CA / self-signed endpoint | set `ca_file` to the CA `.pem` |
|
|
74
|
+
| Hostname mismatch (dev) | `verify_peer_name=False` |
|
|
75
|
+
| Mutual-TLS endpoint | `client_cert` + `client_key` (+ `client_key_passphrase` if the key is encrypted) |
|
|
76
|
+
|
|
77
|
+
The public endpoint on `uaregistry.com:700` is strict RFC EPP and needs **no client
|
|
78
|
+
certificate** — auth is clID + password (over TLS) with an IP allowlist.
|
|
79
|
+
|
|
80
|
+
## Commands
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
# Session
|
|
84
|
+
client.connect(); client.login(); client.logout(); client.disconnect()
|
|
85
|
+
client.login("new-password") # rotate the EPP password during login
|
|
86
|
+
client.hello() # re-read the greeting / keep-alive
|
|
87
|
+
|
|
88
|
+
# Domain
|
|
89
|
+
client.domain.check(["a.com.ua", "b.com.ua"])
|
|
90
|
+
client.domain.info("a.com.ua", "pw")
|
|
91
|
+
client.domain.create("a.com.ua",
|
|
92
|
+
years=1, registrant="C1", contacts={"admin": "C1", "tech": "C2"},
|
|
93
|
+
nameservers=["ns1.x.ua", "ns2.x.ua"], auth_info="pw",
|
|
94
|
+
license="TM-123", # second-level .ua only
|
|
95
|
+
sec_dns={"ds_data": [{"key_tag": 12345, "alg": 8, "digest_type": 2, "digest": "ABCD..."}]})
|
|
96
|
+
client.domain.update("a.com.ua",
|
|
97
|
+
add={"ns": ["ns3.x.ua"], "statuses": ["clientHold"]},
|
|
98
|
+
rem={"statuses": ["clientHold"]},
|
|
99
|
+
chg={"registrant": "C9", "auth_info": "newpw"},
|
|
100
|
+
# DNSSEC (RFC 5910): sec_dns={"add": {"ds_data": [...]}, "rem_all": True, "max_sig_life": 1209600}
|
|
101
|
+
)
|
|
102
|
+
client.domain.renew("a.com.ua", "2027-01-15", 1)
|
|
103
|
+
client.domain.restore("a.com.ua") # RGP restore (op="request")
|
|
104
|
+
client.domain.delete("a.com.ua")
|
|
105
|
+
client.domain.transfer("request", "a.com.ua", "pw", 1)
|
|
106
|
+
|
|
107
|
+
# Contact
|
|
108
|
+
client.contact.check(["c1"])
|
|
109
|
+
client.contact.info("c1", "pw")
|
|
110
|
+
client.contact.create("c1", name="ACME", city="Kyiv", cc="UA", email="a@b.ua", auth_info="pw",
|
|
111
|
+
# postal_infos=[{"type": "int", ...}, {"type": "loc", ...}], # int + localized
|
|
112
|
+
# disclose={"flag": False, "addr": ["int"], "voice": True}, # RFC 5733 privacy
|
|
113
|
+
)
|
|
114
|
+
client.contact.update("c1", chg={"email": "new@b.ua"},
|
|
115
|
+
add_statuses=["clientUpdateProhibited"])
|
|
116
|
+
client.contact.delete("c1")
|
|
117
|
+
client.contact.transfer("request", "c1", "pw")
|
|
118
|
+
|
|
119
|
+
# Host
|
|
120
|
+
client.host.check(["ns1.x.ua"])
|
|
121
|
+
client.host.info("ns1.x.ua")
|
|
122
|
+
client.host.create("ns1.x.ua", ["203.0.113.10", "2001:db8::1"]) # v4/v6 auto-detected
|
|
123
|
+
client.host.update("ns1.x.ua", add_addresses=["203.0.113.11"])
|
|
124
|
+
client.host.delete("ns1.x.ua")
|
|
125
|
+
|
|
126
|
+
# Poll & balance
|
|
127
|
+
msg = client.poll.request() # 1301 with a message, 1300 when empty
|
|
128
|
+
if msg.message_id() is not None: # message_count() = how many remain
|
|
129
|
+
client.poll.ack(msg.message_id())
|
|
130
|
+
b = client.balance().balance() # {"creditLimit": ..., "balance": ..., "availableCredit": ...}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Responses
|
|
134
|
+
|
|
135
|
+
Every command returns a `Response`:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
r.code() # int EPP result code (1000, 1001, 2303, ...)
|
|
139
|
+
r.is_success() # True for 1xxx
|
|
140
|
+
r.is_pending() # True for 1001 (registry resolves via a poll message)
|
|
141
|
+
r.message() # human-readable <msg>
|
|
142
|
+
r.message_lang() # "en" | "uk" | "ua" | "ru"
|
|
143
|
+
r.availability() # {name: bool} for *:check
|
|
144
|
+
r.statuses() # ["ok"] or ["clientHold", ...]
|
|
145
|
+
r.value("exDate") # first element with that local name
|
|
146
|
+
r.values("ns") # all elements with that local name
|
|
147
|
+
r.balance() # {"creditLimit": ..., "balance": ..., "availableCredit": ...} or None
|
|
148
|
+
r.prices() # {"renewal": {"value": ..., "currency": "UAH"}, ...}
|
|
149
|
+
r.license() # .ua trademark/licence number, or None
|
|
150
|
+
r.rgp_status() # ["redemptionPeriod"], ...
|
|
151
|
+
r.transfer_status() # "pending" | "serverApproved" | ... or None
|
|
152
|
+
r.ds_records() # [{"keyTag":..,"alg":..,"digestType":..,"digest":..}, ...]
|
|
153
|
+
r.key_records() # [{"flags":..,"protocol":..,"alg":..,"pubKey":..}, ...]
|
|
154
|
+
r.is_signed() # bool: any DNSSEC data present
|
|
155
|
+
r.message_id() # poll: id to pass to poll().ack(); message_count() = queue size
|
|
156
|
+
r.error_reasons() # extra <extValue><reason> text on a failed command
|
|
157
|
+
r.sv_trid() # server transaction id
|
|
158
|
+
r.raw # the raw XML
|
|
159
|
+
r.root # the parsed ElementTree root, for anything bespoke
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Error handling
|
|
163
|
+
|
|
164
|
+
By default any EPP error code (>= 2000) raises `CommandException` (with `.epp_code` and
|
|
165
|
+
`.response`). Login failures raise `AuthenticationException`; transport problems raise
|
|
166
|
+
`ConnectionException`. All extend `EppException`.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from uaregistry.exceptions import CommandException
|
|
170
|
+
from uaregistry import ResultCode
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
client.domain.create("taken.com.ua", years=1, registrant="C1",
|
|
174
|
+
contacts={"admin": "C1", "tech": "C2"}, nameservers=["ns1.x.ua"])
|
|
175
|
+
except CommandException as exc:
|
|
176
|
+
if exc.epp_code == ResultCode.OBJECT_EXISTS: # 2302
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
# Prefer branching on codes yourself?
|
|
180
|
+
client.throw_on_failure(False)
|
|
181
|
+
resp = client.domain.info("maybe.com.ua")
|
|
182
|
+
if resp.code() == ResultCode.OBJECT_DOES_NOT_EXIST:
|
|
183
|
+
...
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Custom frames
|
|
187
|
+
|
|
188
|
+
Anything the high-level API doesn't cover can be built with `Frame` and sent raw:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from uaregistry import Frame, Namespaces
|
|
192
|
+
|
|
193
|
+
frame = Frame.command("my-trid-1")
|
|
194
|
+
check = frame.ns(frame.verb("check"), Namespaces.DOMAIN, "domain:check")
|
|
195
|
+
frame.ns(check, Namespaces.DOMAIN, "domain:name", "x.com.ua")
|
|
196
|
+
resp = client.request(frame) # or client.request(raw_xml_string)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Testing
|
|
200
|
+
|
|
201
|
+
A no-dependency offline self-test (frame building + response parsing, no server, no network):
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
python tests/offline_test.py
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "uaregistry"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Dependency-free Python client for the UARegistry EPP service (RFC 5730-5734 over TLS)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "UARegistry" }]
|
|
13
|
+
keywords = ["epp", "registry", "domains", "rfc5730", "uaregistry"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.8",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Internet :: Name Service (DNS)",
|
|
26
|
+
]
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/uaregistry/python-sdk"
|
|
31
|
+
Repository = "https://github.com/uaregistry/python-sdk"
|
|
32
|
+
Issues = "https://github.com/uaregistry/python-sdk/issues"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
packages = ["uaregistry"]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""UARegistry EPP SDK (Python).
|
|
2
|
+
|
|
3
|
+
A small, dependency-free client for the UARegistry EPP service — standard RFC 5730-5734
|
|
4
|
+
EPP over TLS on port 700. Speaks the wire protocol directly (no framework, no server code).
|
|
5
|
+
|
|
6
|
+
from uaregistry import Client, Config
|
|
7
|
+
|
|
8
|
+
client = Client(Config(host="uaregistry.com", clid="UAR0001", password="secret"))
|
|
9
|
+
client.connect()
|
|
10
|
+
client.login()
|
|
11
|
+
print(client.domain.check(["example.com.ua"]).availability())
|
|
12
|
+
client.logout()
|
|
13
|
+
client.disconnect()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from . import namespaces as Namespaces
|
|
17
|
+
from .client import Client
|
|
18
|
+
from .commands import Contact, Domain, Host, Poll
|
|
19
|
+
from .config import Config
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
AuthenticationException,
|
|
22
|
+
CommandException,
|
|
23
|
+
ConfigException,
|
|
24
|
+
ConnectionException,
|
|
25
|
+
EppException,
|
|
26
|
+
)
|
|
27
|
+
from .frame import Frame
|
|
28
|
+
from .response import Response
|
|
29
|
+
from .result_code import ResultCode
|
|
30
|
+
from .transport import Connection, Transport
|
|
31
|
+
|
|
32
|
+
__version__ = "1.0.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"Client",
|
|
36
|
+
"Config",
|
|
37
|
+
"Frame",
|
|
38
|
+
"Response",
|
|
39
|
+
"ResultCode",
|
|
40
|
+
"Namespaces",
|
|
41
|
+
"Transport",
|
|
42
|
+
"Connection",
|
|
43
|
+
"Domain",
|
|
44
|
+
"Contact",
|
|
45
|
+
"Host",
|
|
46
|
+
"Poll",
|
|
47
|
+
"EppException",
|
|
48
|
+
"ConnectionException",
|
|
49
|
+
"ConfigException",
|
|
50
|
+
"CommandException",
|
|
51
|
+
"AuthenticationException",
|
|
52
|
+
"__version__",
|
|
53
|
+
]
|