epptools 1.0.0__tar.gz → 1.0.2__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.
Files changed (60) hide show
  1. {epptools-1.0.0 → epptools-1.0.2}/CHANGELOG.md +44 -0
  2. {epptools-1.0.0/epptools.egg-info → epptools-1.0.2}/PKG-INFO +1 -1
  3. {epptools-1.0.0 → epptools-1.0.2}/docs/en/builders.md +22 -11
  4. {epptools-1.0.0 → epptools-1.0.2}/docs/en/contacts.md +29 -17
  5. {epptools-1.0.0 → epptools-1.0.2}/docs/ru/builders.md +22 -13
  6. {epptools-1.0.0 → epptools-1.0.2}/docs/ru/contacts.md +29 -17
  7. {epptools-1.0.0 → epptools-1.0.2}/docs/uk/builders.md +22 -11
  8. {epptools-1.0.0 → epptools-1.0.2}/docs/uk/contacts.md +28 -17
  9. {epptools-1.0.0 → epptools-1.0.2}/epptools/_version.py +1 -1
  10. {epptools-1.0.0 → epptools-1.0.2}/epptools/builders.py +16 -7
  11. {epptools-1.0.0 → epptools-1.0.2}/epptools/commands.py +51 -8
  12. {epptools-1.0.0 → epptools-1.0.2/epptools.egg-info}/PKG-INFO +1 -1
  13. {epptools-1.0.0 → epptools-1.0.2}/pyproject.toml +1 -1
  14. {epptools-1.0.0 → epptools-1.0.2}/LICENSE +0 -0
  15. {epptools-1.0.0 → epptools-1.0.2}/MANIFEST.in +0 -0
  16. {epptools-1.0.0 → epptools-1.0.2}/README.md +0 -0
  17. {epptools-1.0.0 → epptools-1.0.2}/docs/en/README.md +0 -0
  18. {epptools-1.0.0 → epptools-1.0.2}/docs/en/balance.md +0 -0
  19. {epptools-1.0.0 → epptools-1.0.2}/docs/en/commands.md +0 -0
  20. {epptools-1.0.0 → epptools-1.0.2}/docs/en/domains.md +0 -0
  21. {epptools-1.0.0 → epptools-1.0.2}/docs/en/errors.md +0 -0
  22. {epptools-1.0.0 → epptools-1.0.2}/docs/en/hosts.md +0 -0
  23. {epptools-1.0.0 → epptools-1.0.2}/docs/en/poll.md +0 -0
  24. {epptools-1.0.0 → epptools-1.0.2}/docs/en/quickstart.md +0 -0
  25. {epptools-1.0.0 → epptools-1.0.2}/docs/en/responses.md +0 -0
  26. {epptools-1.0.0 → epptools-1.0.2}/docs/en/session.md +0 -0
  27. {epptools-1.0.0 → epptools-1.0.2}/docs/ru/README.md +0 -0
  28. {epptools-1.0.0 → epptools-1.0.2}/docs/ru/balance.md +0 -0
  29. {epptools-1.0.0 → epptools-1.0.2}/docs/ru/commands.md +0 -0
  30. {epptools-1.0.0 → epptools-1.0.2}/docs/ru/domains.md +0 -0
  31. {epptools-1.0.0 → epptools-1.0.2}/docs/ru/errors.md +0 -0
  32. {epptools-1.0.0 → epptools-1.0.2}/docs/ru/hosts.md +0 -0
  33. {epptools-1.0.0 → epptools-1.0.2}/docs/ru/poll.md +0 -0
  34. {epptools-1.0.0 → epptools-1.0.2}/docs/ru/quickstart.md +0 -0
  35. {epptools-1.0.0 → epptools-1.0.2}/docs/ru/responses.md +0 -0
  36. {epptools-1.0.0 → epptools-1.0.2}/docs/ru/session.md +0 -0
  37. {epptools-1.0.0 → epptools-1.0.2}/docs/uk/README.md +0 -0
  38. {epptools-1.0.0 → epptools-1.0.2}/docs/uk/balance.md +0 -0
  39. {epptools-1.0.0 → epptools-1.0.2}/docs/uk/commands.md +0 -0
  40. {epptools-1.0.0 → epptools-1.0.2}/docs/uk/domains.md +0 -0
  41. {epptools-1.0.0 → epptools-1.0.2}/docs/uk/errors.md +0 -0
  42. {epptools-1.0.0 → epptools-1.0.2}/docs/uk/hosts.md +0 -0
  43. {epptools-1.0.0 → epptools-1.0.2}/docs/uk/poll.md +0 -0
  44. {epptools-1.0.0 → epptools-1.0.2}/docs/uk/quickstart.md +0 -0
  45. {epptools-1.0.0 → epptools-1.0.2}/docs/uk/responses.md +0 -0
  46. {epptools-1.0.0 → epptools-1.0.2}/docs/uk/session.md +0 -0
  47. {epptools-1.0.0 → epptools-1.0.2}/epptools/__init__.py +0 -0
  48. {epptools-1.0.0 → epptools-1.0.2}/epptools/client.py +0 -0
  49. {epptools-1.0.0 → epptools-1.0.2}/epptools/config.py +0 -0
  50. {epptools-1.0.0 → epptools-1.0.2}/epptools/exceptions.py +0 -0
  51. {epptools-1.0.0 → epptools-1.0.2}/epptools/frame.py +0 -0
  52. {epptools-1.0.0 → epptools-1.0.2}/epptools/namespaces.py +0 -0
  53. {epptools-1.0.0 → epptools-1.0.2}/epptools/py.typed +0 -0
  54. {epptools-1.0.0 → epptools-1.0.2}/epptools/response.py +0 -0
  55. {epptools-1.0.0 → epptools-1.0.2}/epptools/result_code.py +0 -0
  56. {epptools-1.0.0 → epptools-1.0.2}/epptools/transport.py +0 -0
  57. {epptools-1.0.0 → epptools-1.0.2}/epptools.egg-info/SOURCES.txt +0 -0
  58. {epptools-1.0.0 → epptools-1.0.2}/epptools.egg-info/dependency_links.txt +0 -0
  59. {epptools-1.0.0 → epptools-1.0.2}/epptools.egg-info/top_level.txt +0 -0
  60. {epptools-1.0.0 → epptools-1.0.2}/setup.cfg +0 -0
@@ -4,6 +4,50 @@ All notable changes to this library are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.0.2]
8
+
9
+ ### Fixed
10
+
11
+ - **A postal change that carried only part of the block could delete the rest of the address.** A
12
+ `<contact:postalInfo>` inside `<contact:chg>` is not merged field by field by every registry: one
13
+ that *replaces* the block stores exactly what you sent and drops everything else — while answering
14
+ **1000**. Clearing an organisation on its own therefore left a contact with no name, no street, no
15
+ city, no postal code and no country, and nothing in the response said so. `name`, `city` and `cc`
16
+ are now required in every postal change, through `contact.update()` and the update builder alike.
17
+
18
+ ### Changed
19
+
20
+ - A postal change missing `name`, `city` or `cc` now raises `ValidationException` instead of being
21
+ sent. This rejects calls that previously appeared to succeed — those were the calls losing data.
22
+ Read the current block with `client.contact.info(id).postal_info()` and send it back with your
23
+ change applied.
24
+ - The documentation for partial postal updates was wrong in the same way and has been rewritten. A
25
+ block is replaced, not merged: a field you leave out is deleted, not preserved.
26
+
27
+ ## [1.0.1]
28
+
29
+ ### Fixed
30
+
31
+ - **Clearing one optional part of an address built a frame the server rejects.** RFC 5733 makes
32
+ `<contact:addr>` a sequence with a required `city` and `cc`, so a change to any part of it sends
33
+ the whole block. What the library did was substitute an EMPTY STRING for whatever the caller had
34
+ not supplied — so `{"sp": ""}`, the documented way to remove a state, went out with `<city/>` and
35
+ `<cc/>` beside it. Both are schema-invalid (`postalLineType` has minLength 1, `ccType` is exactly
36
+ two characters), and an invalid frame comes back as a bare `2001` naming no element. A caller
37
+ following the manual got the least useful error in EPP.
38
+
39
+ A partial address change now requires `city` and `cc` and says so before anything is sent. Read
40
+ them back from `info()` and pass them through unchanged alongside what you are changing.
41
+
42
+ - **An empty value is now refused for the fields the schema forbids it on.** Which parts of a
43
+ contact can be CLEARED is fixed by `contact-1.0.xsd` and not by convention: `org`, `street`, `sp`
44
+ and `pc` have types with no minimum length, so sending them empty is what removes them; `name`,
45
+ `city` and `cc` do not, so an empty one cannot be sent at all. Passing one is answered here, with
46
+ the reason, instead of costing a round trip.
47
+
48
+ Neither changes a call that worked. Both turn a call the server was already rejecting into one that
49
+ fails immediately and says why.
50
+
7
51
  ## [1.0.0]
8
52
 
9
53
  First public release.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: epptools
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Dependency-free Python client for EPP domain registries (RFC 5730-5734 over TLS), with a registry's own extensions discovered from its greeting.
5
5
  Author: EppTools
6
6
  License: MIT
@@ -398,24 +398,35 @@ Statuses go in their own blocks; every field change lands in `chg`.
398
398
  .send())
399
399
  ```
400
400
 
401
- ### Presence decides, and an empty string clears
401
+ ### An address is REPLACED, not merged
402
402
 
403
- Inside an address, an argument you do not pass is not sent at all, so the registry keeps what it
404
- holds. An argument you pass as `""` **is** sent, empty, which is what clears a field — and it is the
405
- only way to remove an `org`, a state province or a postal code:
403
+ The block you pass **replaces** the one the registry holds. It is not merged field by field, so an
404
+ argument you do not pass is not sent — and the registry deletes what it held. An argument passed as
405
+ `""` is sent empty, which is what clears a field.
406
+
407
+ RFC 5733 can be read as "leave it out and the registry keeps what it holds", since every child of
408
+ `chgPostalInfoType` is optional, but that reading is not safe. Against a registry that replaces —
409
+ **every command answering 1000** — a block sent without its `org` comes back with the organisation
410
+ gone, and a block carrying only an `org` leaves the contact with no postal address at all: name,
411
+ street, city, postal code and country.
412
+
413
+ `name`, `city` and `country_code` are required in every address change for that reason, and the
414
+ builder refuses the call without them. They keep the frame valid; they cannot restore an argument you
415
+ did not pass. **Read the block first and pass it back with your change applied:**
406
416
 
407
417
  ```python
408
- # Remove the organisation, leave the rest of the address alone.
418
+ current = client.contact.info("C1").postal_info()["int"]
419
+
420
+ # Remove the organisation, keeping the rest of the address exactly as it was.
409
421
  (client.contact.update_builder("C1")
410
- .change_international_address(org="", city="Kyiv", country_code="UA")
422
+ .change_international_address(name=current["name"], city=current["city"],
423
+ country_code=current["cc"], street=current.get("street"),
424
+ org="")
411
425
  .send())
412
426
  ```
413
427
 
414
- **Give `city` and `country_code` whenever you touch the address at all.** The `<contact:addr>`
415
- element is a sequence whose city and country the schema requires, so it is sent whole or not at all
416
- — and when it is sent, city and country go with it. Leaving them out of a call that changes a street
417
- line sends them empty, and empty is a deliberate clear. The command answers 1000 and the contact
418
- loses its city and country.
428
+ The form you do not mention — local or international — is untouched: the two are addressed
429
+ separately.
419
430
 
420
431
  Changing one postal form leaves the other exactly as it was.
421
432
 
@@ -325,31 +325,43 @@ There is no way to *remove* a contact's transfer secret. RFC 5731 gives a domain
325
325
  RFC 5733 defines no equivalent for a contact, so a contact's code can be replaced but not cleared.
326
326
  An empty password is not a substitute: an empty value is still a value the holder can present.
327
327
 
328
- ### The partial-update rule: presence decides
328
+ ### The partial-update rule: a postal block is REPLACED, not merged
329
329
 
330
- Inside a postal block, **presence decides and an empty string clears**:
330
+ Send a postal block in `chg` and the registry **replaces** the block it holds with the one you sent.
331
+ The two are not merged field by field, so whatever you leave out is gone.
331
332
 
332
- | You write | What happens |
333
+ RFC 5733 can be read the other way: in `chgPostalInfoType` name, org and addr are each optional,
334
+ which looks like "leave it out and the registry keeps what it holds". That reading is not safe.
335
+ Against a registry that replaces, **every one of these commands answers 1000**:
336
+
337
+ | What the `chg` carried | What the contact had afterwards |
333
338
  |---|---|
334
- | the key is absent | the field is not sent, and the registry keeps what it holds |
335
- | the key holds a value | the field is set to it |
336
- | the key holds `""` | the field is sent empty, which **clears** it |
339
+ | the complete block, `"org": ""` | organisation removed, address untouched |
340
+ | the complete block, no `org` key | organisation **also removed** |
341
+ | `{"org": ""}` and nothing else | **no postal block at all** — name, street, city, postal code and country gone |
342
+
343
+ So there is no such thing as changing one field of an address, and the failure is silent: the command
344
+ succeeds and the data is gone. `name`, `city` and `cc` are required in every postal change and the
345
+ SDK refuses the call without them, but that guard only keeps the frame schema-valid — it cannot put
346
+ back an `org`, an `sp` or a `pc` you did not send.
337
347
 
338
- That is the only way to remove an `org`, an `sp` or a `pc`. It is also the only postal form that is
339
- touched: changing the `int` block leaves the `loc` block exactly as it was, and the other way round.
348
+ **Read the block, apply your change, send it back whole:**
340
349
 
341
350
  ```python
342
- # Remove the organisation, change nothing else about the address.
343
- client.contact.update("C1",
344
- chg={"postal_info": {"type": "int", "org": "",
345
- "city": "Kyiv", "cc": "UA"}})
351
+ current = client.contact.info("C1").postal_info()["int"]
352
+ client.contact.update("C1", chg={"postal_info": {**current, "type": "int", "org": ""}})
346
353
  ```
347
354
 
348
- **Give `city` and `cc` whenever you touch the address at all.** The `<contact:addr>` element is a
349
- sequence whose city and country are required by the schema, so it is sent whole or not at all — and
350
- when it is sent, city and country go with it. Leaving them out of a call that changes a street line
351
- sends them **empty**, and an empty value is a deliberate clear. The command answers 1000 and the
352
- contact loses its city and country.
355
+ One thing the replacement does *not* reach is the other postal form: `int` and `loc` are addressed
356
+ separately, so replacing one leaves the other exactly as it was.
357
+
358
+ Inside the block you send, an empty string is still what clears an optional field:
359
+
360
+ | You write | What happens |
361
+ |---|---|
362
+ | the key holds a value | the field is set to it |
363
+ | the key holds `""` | the field is sent empty, which **clears** it |
364
+ | the key is absent | the field is not sent — and the registry deletes what it held |
353
365
 
354
366
  Street lines behave differently and in your favour: an address block sent without any `street`
355
367
  leaves the existing lines untouched.
@@ -401,26 +401,35 @@ handle = (client.contact.create_builder(Contact.AUTO_ID, "contact@example.com")
401
401
  .send())
402
402
  ```
403
403
 
404
- ### Решает присутствие, а пустая строка очищает
404
+ ### Адрес ЗАМЕЩАЕТСЯ целиком, а не сливается по полям
405
405
 
406
- Внутри адреса аргумент, которого вы не передали, не отправляется вовсе, поэтому реестр сохраняет то,
407
- что у него есть. Аргумент, переданный как `""`, **отправляется** пустым, и именно это очищает поле —
408
- и это единственный способ убрать `org`, область или почтовый индекс:
406
+ Блок, который вы передаёте, **замещает** тот, что хранит реестр. Их не сливают поле за полем, поэтому
407
+ аргумент, которого вы не передали, не отправляется — и реестр удаляет то, что хранил. Аргумент,
408
+ переданный как `""`, отправляется пустым, и именно это очищает поле.
409
+
410
+ RFC 5733 можно прочитать как «не передавайте — и реестр сохранит то, что хранит», ведь каждая
411
+ составляющая `chgPostalInfoType` необязательна, но это чтение небезопасно. Против реестра, который
412
+ замещает блок, — причём **каждая команда отвечает 1000** — блок, отправленный без `org`,
413
+ возвращается уже без организации, а блок, в котором был один лишь `org`, оставляет контакт вообще
414
+ без почтового адреса: без имени, улицы, города, индекса и страны.
415
+
416
+ Именно поэтому `name`, `city` и `country_code` обязательны в любом изменении адреса, и билдер без
417
+ них отказывает. Они удерживают кадр валидным, но вернуть аргумент, которого вы не передали, не
418
+ могут. **Сначала прочитайте блок и верните его вместе со своим изменением:**
409
419
 
410
420
  ```python
411
- # Убрать организацию, остальной адрес не трогать.
421
+ current = client.contact.info("C1").postal_info()["int"]
422
+
423
+ # Убрать организацию, оставив остальной адрес ровно таким, каким он был.
412
424
  (client.contact.update_builder("C1")
413
- .change_international_address(org="", city="Kyiv", country_code="UA")
425
+ .change_international_address(name=current["name"], city=current["city"],
426
+ country_code=current["cc"], street=current.get("street"),
427
+ org="")
414
428
  .send())
415
429
  ```
416
430
 
417
- **Передавайте `city` и `country_code` всякий раз, когда вообще трогаете адрес.** Элемент
418
- `<contact:addr>` — это последовательность, город и страну в которой требует схема, поэтому он
419
- отправляется целиком или не отправляется вовсе, — а раз отправляется, то вместе с городом и страной.
420
- Не указать их в вызове, который меняет строку улицы, значит отправить их пустыми, а пустое — это
421
- осознанная очистка. Команда ответит 1000, и контакт лишится города и страны.
422
-
423
- Изменение одной почтовой формы оставляет другую ровно такой, какой она была.
431
+ Изменение одной почтовой формы оставляет другую ровно такой, какой она была: они адресуются
432
+ раздельно.
424
433
 
425
434
  ### Здесь нет clear_auth_info()
426
435
 
@@ -331,31 +331,43 @@ RFC 5733 ничего равнозначного для контакта не о
331
331
  очистить. Пустой пароль заменой не является: пустое значение — это по-прежнему значение, которое
332
332
  держатель может предъявить.
333
333
 
334
- ### Правило частичного изменения: решает присутствие
334
+ ### Правило изменения: почтовый блок ЗАМЕЩАЕТСЯ, а не сливается
335
335
 
336
- Внутри почтового блока **решает присутствие ключа, а пустая строка очищает**:
336
+ Отправьте почтовый блок в `chg` — и реестр **заместит** им тот, что хранит. Их не сливают поле за
337
+ полем, поэтому всё, чего вы не передали, исчезает.
337
338
 
338
- | Что вы пишете | Что происходит |
339
+ RFC 5733 можно прочитать и иначе: в `chgPostalInfoType` каждая из составляющих — name, org, addr —
340
+ необязательна, и это выглядит как «не передавайте её, и реестр сохранит то, что хранит». Это чтение
341
+ небезопасно: против реестра, который замещает блок, **каждая из этих команд отвечает 1000**:
342
+
343
+ | Что нёс `chg` | Что осталось у контакта |
339
344
  |---|---|
340
- | ключа нет | поле не отправляется, и реестр сохраняет то, что хранит |
341
- | ключ несёт значение | поле принимает это значение |
342
- | ключ несёт `""` | поле отправляется пустым, что его **очищает** |
345
+ | полный блок с `"org": ""` | организация убрана, адрес нетронут |
346
+ | полный блок без ключа `org` | организация **тоже убрана** |
347
+ | один лишь `{"org": ""}` | **почтового блока нет вообще** — ни имени, ни улицы, города, индекса и страны |
348
+
349
+ То есть изменить одно поле адреса невозможно, и сбой молчаливый: команда успешна, а данных нет.
350
+ `name`, `city` и `cc` обязательны в любом почтовом изменении, и этот SDK без них отказывает, — но
351
+ эта проверка лишь удерживает кадр валидным и вернуть `org`, `sp` или `pc`, которого вы не передали,
352
+ не может.
343
353
 
344
- Только так и можно убрать `org`, `sp` или `pc`. И затрагивается только та почтовая форма, о которой
345
- идёт речь: изменение блока `int` оставляет блок `loc` ровно таким, каким он был, и наоборот.
354
+ **Прочитайте блок, примените своё изменение и отправьте его целиком:**
346
355
 
347
356
  ```python
348
- # Убрать организацию, не меняя в адресе больше ничего.
349
- client.contact.update("C1",
350
- chg={"postal_info": {"type": "int", "org": "",
351
- "city": "Kyiv", "cc": "UA"}})
357
+ current = client.contact.info("C1").postal_info()["int"]
358
+ client.contact.update("C1", chg={"postal_info": {**current, "type": "int", "org": ""}})
352
359
  ```
353
360
 
354
- **Указывайте `city` и `cc` всякий раз, когда вообще трогаете адрес.** Элемент `<contact:addr>` — это
355
- последовательность, город и страна в которой обязательны по схеме, поэтому он отправляется целиком
356
- или не отправляется вовсе, а если отправляется, то вместе с городом и страной. Не указав их в
357
- вызове, который меняет строку улицы, вы отправите их **пустыми**, а пустое значение — это
358
- намеренная очистка. Команда ответит 1000, и контакт лишится города и страны.
361
+ Чего замещение не достаёт — так это второй почтовой формы: `int` и `loc` адресуются раздельно,
362
+ поэтому замена одной оставляет другую ровно такой, какой она была.
363
+
364
+ Внутри блока, который вы отправляете, пустая строка по-прежнему очищает необязательное поле:
365
+
366
+ | Что вы пишете | Что происходит |
367
+ |---|---|
368
+ | ключ несёт значение | поле принимает это значение |
369
+ | ключ несёт `""` | поле отправляется пустым, что его **очищает** |
370
+ | ключа нет | поле не отправляется — и реестр удаляет то, что хранил |
359
371
 
360
372
  Со строками улицы всё иначе, и в вашу пользу: блок адреса, отправленный без `street`, оставляет
361
373
  существующие строки нетронутыми.
@@ -399,24 +399,35 @@ handle = (client.contact.create_builder(Contact.AUTO_ID, "contact@example.com")
399
399
  .send())
400
400
  ```
401
401
 
402
- ### Вирішує наявність, а порожній рядок очищає
402
+ ### Адресу ЗАМІЩУЮТЬ цілком, а не зливають по полях
403
403
 
404
- Усередині адреси аргумент, якого ви не передали, не надсилається взагалі, тож реєстр зберігає те, що
405
- тримає. Аргумент, переданий як `""`, **надсилається** — порожнім, і саме це очищає поле; це єдиний
406
- спосіб прибрати `org`, область чи поштовий індекс:
404
+ Блок, який ви передаєте, **заміщає** той, що тримає реєстр. Їх не зливають поле за полем, тож
405
+ аргумент, якого ви не передали, не надсилається — і реєстр видаляє те, що тримав. Аргумент,
406
+ переданий як `""`, надсилається порожнім, і саме це очищає поле.
407
+
408
+ RFC 5733 можна прочитати як «не подавайте — і реєстр збереже те, що має», бо кожна складова
409
+ `chgPostalInfoType` необов'язкова, але це читання небезпечне. Проти реєстру, який заміщає блок, —
410
+ причому **кожна команда відповідає 1000** — блок, надісланий без `org`, повертається вже без
411
+ організації, а блок, у якому був самий лише `org`, лишає контакт узагалі без поштової адреси: без
412
+ імені, вулиці, міста, індексу та країни.
413
+
414
+ Саме тому `name`, `city` і `country_code` обов'язкові в кожній зміні адреси, і білдер без них
415
+ відмовляє. Вони тримають кадр валідним, але не здатні повернути аргумент, якого ви не передали.
416
+ **Спершу прочитайте блок і поверніть його разом зі своєю зміною:**
407
417
 
408
418
  ```python
409
- # Прибрати організацію, решту адреси не чіпати.
419
+ current = client.contact.info("C1").postal_info()["int"]
420
+
421
+ # Прибрати організацію, лишивши решту адреси точно такою, як була.
410
422
  (client.contact.update_builder("C1")
411
- .change_international_address(org="", city="Kyiv", country_code="UA")
423
+ .change_international_address(name=current["name"], city=current["city"],
424
+ country_code=current["cc"], street=current.get("street"),
425
+ org="")
412
426
  .send())
413
427
  ```
414
428
 
415
- **Указуйте `city` і `country_code`, щойно ви взагалі торкаєтесь адреси.** Елемент `<contact:addr>` є
416
- послідовністю, чиї місто і країну вимагає схема, тож він надсилається цілком або не надсилається
417
- зовсім, — а коли надсилається, місто і країна їдуть разом із ним. Якщо не вказати їх у виклику, який
418
- змінює рядок вулиці, вони підуть порожніми, а порожнє — це свідоме очищення. Команда відповість 1000,
419
- і контакт втратить своє місто та країну.
429
+ Форма, якої ви не згадали — локальна чи міжнародна, — лишається недоторканою: вони адресуються
430
+ окремо.
420
431
 
421
432
  Зміна однієї поштової форми лишає другу рівно такою, як була.
422
433
 
@@ -328,31 +328,42 @@ RFC 5733 не визначає для контакту жодного відпо
328
328
  очистити. Порожній пароль замінником не є: порожнє значення — це все одно значення, яке той, хто
329
329
  його має, може пред'явити.
330
330
 
331
- ### Правило часткового оновлення: вирішує присутність
331
+ ### Правило оновлення: поштовий блок ЗАМІЩУЄТЬСЯ, а не зливається
332
332
 
333
- Усередині поштового блоку **вирішує присутність, а порожній рядок очищає**:
333
+ Надішліть поштовий блок у `chg` — і реєстр **замістить** ним той, що тримає. Їх не зливають поле за
334
+ полем, тож усе, чого ви не подали, зникає.
334
335
 
335
- | Ви пишете | Що відбувається |
336
+ RFC 5733 можна прочитати й інакше: у `chgPostalInfoType` кожна зі складових — name, org, addr —
337
+ необов'язкова, і це схоже на «не подавайте її, і реєстр збереже те, що має». Це читання небезпечне:
338
+ проти реєстру, який заміщає блок, **кожна з цих команд відповідає 1000**:
339
+
340
+ | Що ніс `chg` | Що лишилося в контакті |
336
341
  |---|---|
337
- | ключа немає | поле не надсилається, і реєстр зберігає те, що має |
338
- | ключ має значення | поле встановлюється в це значення |
339
- | ключ містить `""` | поле надсилається порожнім, що **очищає** його |
342
+ | повний блок із `"org": ""` | організацію прибрано, адреса недоторкана |
343
+ | повний блок без ключа `org` | організацію **також прибрано** |
344
+ | самий лише `{"org": ""}` | **поштового блоку немає взагалі** — ані імені, ані вулиці, міста, індексу чи країни |
345
+
346
+ Тобто змінити одне поле адреси неможливо, і збій мовчазний: команда успішна, а даних немає. `name`,
347
+ `city` і `cc` обов'язкові в кожній поштовій зміні, і цей SDK без них відмовляє, — але ця перевірка
348
+ лише тримає кадр валідним і не здатна повернути `org`, `sp` чи `pc`, якого ви не подали.
340
349
 
341
- Це єдиний спосіб прибрати `org`, `sp` чи `pc`. І це також єдина поштова форма, якої торкаються:
342
- зміна блоку `int` лишає блок `loc` точно таким, як був, і навпаки.
350
+ **Прочитайте блок, застосуйте свою зміну і надішліть його цілим:**
343
351
 
344
352
  ```python
345
- # Прибрати організацію, більше нічого в адресі не змінювати.
346
- client.contact.update("C1",
347
- chg={"postal_info": {"type": "int", "org": "",
348
- "city": "Kyiv", "cc": "UA"}})
353
+ current = client.contact.info("C1").postal_info()["int"]
354
+ client.contact.update("C1", chg={"postal_info": {**current, "type": "int", "org": ""}})
349
355
  ```
350
356
 
351
- **Вказуйте `city` та `cc` щоразу, коли взагалі торкаєтеся адреси.** Елемент `<contact:addr>` — це
352
- послідовність, у якій місто й країну вимагає схема, тож він надсилається цілком або не надсилається
353
- зовсім, — а коли надсилається, місто й країна йдуть разом з ним. Пропустивши їх у виклику, що змінює
354
- рядок вулиці, ви надсилаєте їх **порожніми**, а порожнє значення — це навмисне очищення. Команда
355
- відповідає 1000, і контакт втрачає своє місто й країну.
357
+ Чого заміщення не досягає — то це другої поштової форми: `int` і `loc` адресуються окремо, тож
358
+ заміна однієї лишає другу точно такою, як була.
359
+
360
+ Усередині блоку, який ви надсилаєте, порожній рядок так само очищає необов'язкове поле:
361
+
362
+ | Ви пишете | Що відбувається |
363
+ |---|---|
364
+ | ключ має значення | поле встановлюється в це значення |
365
+ | ключ містить `""` | поле надсилається порожнім, що **очищає** його |
366
+ | ключа немає | поле не надсилається — і реєстр видаляє те, що тримав |
356
367
 
357
368
  Рядки вулиці поводяться інакше і на вашу користь: блок адреси, надісланий без жодного `street`,
358
369
  лишає наявні рядки недоторканими.
@@ -4,4 +4,4 @@ Kept in its own module so ``client`` can report it (RFC 8807 ``<loginSec:app>``)
4
4
  the package root, which imports ``client`` in turn.
5
5
  """
6
6
 
7
- __version__ = "1.0.0"
7
+ __version__ = "1.0.2"
@@ -508,18 +508,27 @@ class ContactUpdateBuilder(Builder):
508
508
  org: Optional[str] = None,
509
509
  state_province: Optional[str] = None,
510
510
  postal_code: Optional[str] = None) -> "ContactUpdateBuilder":
511
- """Change the ASCII address. Only the fields you pass are sent, and PRESENCE decides:
511
+ """Change the ASCII address.
512
+
513
+ THE BLOCK YOU PASS REPLACES THE ONE THE REGISTRY HOLDS. It is not merged field by field,
514
+ so anything you leave out is deleted:
512
515
 
513
516
  ================== ===========================================================
514
- leave it out/None the field is not sent, and the registry keeps its value
515
517
  give a value the field is set to it
516
- give ``""`` the field is CLEARED — the only way to remove org, state
517
- province or postal code
518
+ give ``""`` the field is CLEARED — the way to remove org, state province
519
+ or postal code
520
+ leave it out/None the field is not sent, and the registry DELETES what it held
518
521
  ================== ===========================================================
519
522
 
520
- The other form (local or international) is untouched. The address block is a sequence with
521
- a required city and country, so passing any part of it sends the whole block: give
522
- ``city`` and ``country_code`` whenever you touch street, state province or postal code.
523
+ RFC 5733 can be read as "leave it out and the registry keeps its value", since every child
524
+ of chgPostalInfoType is optional, but that reading is not safe: against a registry that
525
+ replaces, a block sent without its org comes back 1000 with the org gone, and a block
526
+ carrying only an org leaves the contact with no postal address at all. ``name``, ``city``
527
+ and ``country_code`` are required here for that reason — but they only keep the frame valid,
528
+ they cannot restore a field you did not send. Read the current block with ``contact.info()``
529
+ and pass it back with your change applied.
530
+
531
+ The other form (local or international) IS untouched — the two are addressed separately.
523
532
  """
524
533
  return self._address("int", name, city, country_code, street, org, state_province, postal_code)
525
534
 
@@ -706,24 +706,67 @@ def _append_postal(frame: Frame, parent: ET.Element, pi: Dict[str, Any],
706
706
  """
707
707
  block = frame.ns(parent, _ADMIN, "contact:postalInfo", None, {"type": pi.get("type") or "int"})
708
708
 
709
- if not partial or "name" in pi:
710
- frame.ns(block, _ADMIN, "contact:name", pi.get("name") or "")
709
+ # A postalInfo inside <contact:chg> REPLACES the stored one. It is not merged field by field.
710
+ #
711
+ # RFC 5733 can be read the other way: in chgPostalInfoType each of name/org/addr is optional,
712
+ # which looks like "omit it and the registry keeps what it holds". That reading is not safe.
713
+ # Against a registry that replaces, a chg carrying only <contact:org/> answers **1000** and leaves
714
+ # the contact with NO postalInfo at all — name, street, city, postal code and country gone, in
715
+ # both the int and loc blocks — and a complete block sent without an <org> removes the
716
+ # organisation just as surely.
717
+ #
718
+ # So the short form does not fail, it DESTROYS, and it reports success while doing it. A client
719
+ # cannot tell a replacing registry from a merging one, and the cost of guessing wrong is a
720
+ # registrant's postal address. Every change therefore carries the whole block.
721
+ for required in ("name", "city", "cc"):
722
+ if not str(pi.get(required) or "").strip():
723
+ raise ValidationException(
724
+ "postalInfo: a <contact:postalInfo> is REPLACED as a whole, not merged, so every "
725
+ f'change must carry the complete block — "{required}" is missing. (A registry that '
726
+ "replaces answers 1000 and silently drops everything you left out.) Read the current "
727
+ "block with contact.info() and send it back with your change applied."
728
+ )
729
+
730
+ # name is postalLineType, minLength 1: there is NO WAY to clear a name. An empty element is
731
+ # schema-invalid and the server answers a bare 2001 naming no field — refused here, where the
732
+ # message can say so.
733
+ _require_not_empty(pi.get("name"), "name")
734
+ frame.ns(block, _ADMIN, "contact:name", pi["name"])
735
+
736
+ # org is optPostalLineType, which HAS no minLength — an empty one is legal and is exactly how an
737
+ # organisation is removed.
711
738
  if ("org" in pi) if partial else bool(pi.get("org")):
712
739
  frame.ns(block, _ADMIN, "contact:org", pi.get("org") or "")
713
740
 
714
- # <addr> is a sequence with a required city and cc, so it is emitted whole or not at all.
715
- addr_keys = ("street", "city", "sp", "pc", "cc")
716
- if partial and not any(k in pi for k in addr_keys):
717
- return
741
+ # <addr> is a sequence with a required city and cc, and it is always emitted: the block replaces
742
+ # what the registry holds, so leaving the address out is what deletes it. The required parts were
743
+ # already asserted above, before anything was written.
744
+
718
745
  addr = frame.ns(block, _ADMIN, "contact:addr")
719
746
  for line in (pi.get("street") or []):
720
747
  frame.ns(addr, _ADMIN, "contact:street", line)
721
- frame.ns(addr, _ADMIN, "contact:city", pi.get("city") or "")
748
+ frame.ns(addr, _ADMIN, "contact:city", pi["city"])
722
749
  if ("sp" in pi) if partial else bool(pi.get("sp")):
723
750
  frame.ns(addr, _ADMIN, "contact:sp", pi.get("sp") or "")
724
751
  if ("pc" in pi) if partial else bool(pi.get("pc")):
725
752
  frame.ns(addr, _ADMIN, "contact:pc", pi.get("pc") or "")
726
- frame.ns(addr, _ADMIN, "contact:cc", pi.get("cc") or "")
753
+ frame.ns(addr, _ADMIN, "contact:cc", pi["cc"])
754
+
755
+
756
+ def _require_not_empty(value: Any, field: str) -> None:
757
+ """Refuse an empty value for an element whose schema type forbids one.
758
+
759
+ The distinction is not a house rule, it is contact-1.0.xsd: ``optPostalLineType`` (org, street,
760
+ sp) has no minLength and ``pcType`` has none either, so those four clear by being sent empty.
761
+ ``postalLineType`` (name, city) has minLength 1 and ``ccType`` is exactly two characters, so an
762
+ empty one of those cannot be sent at all. Getting it wrong costs a round trip and returns a bare
763
+ 2001 with no field named — the least useful error in EPP.
764
+ """
765
+ if not str(value or "").strip():
766
+ raise ValidationException(
767
+ 'postalInfo: "%s" cannot be empty — RFC 5733 gives it a schema type with a minimum '
768
+ "length, so there is no way to clear it. Omit the key to leave it unchanged." % field
769
+ )
727
770
 
728
771
 
729
772
  def _append_disclose(frame: Frame, parent: ET.Element, disclose: Dict[str, Any]) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: epptools
3
- Version: 1.0.0
3
+ Version: 1.0.2
4
4
  Summary: Dependency-free Python client for EPP domain registries (RFC 5730-5734 over TLS), with a registry's own extensions discovered from its greeting.
5
5
  Author: EppTools
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "epptools"
7
- version = "1.0.0"
7
+ version = "1.0.2"
8
8
  description = "Dependency-free Python client for EPP domain registries (RFC 5730-5734 over TLS), with a registry's own extensions discovered from its greeting."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes