binaaz-python-sdk 0.1.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.
- binaaz_python_sdk-0.1.0/.gitignore +14 -0
- binaaz_python_sdk-0.1.0/CHANGELOG.md +17 -0
- binaaz_python_sdk-0.1.0/LICENSE +21 -0
- binaaz_python_sdk-0.1.0/PKG-INFO +205 -0
- binaaz_python_sdk-0.1.0/README.md +163 -0
- binaaz_python_sdk-0.1.0/examples/binaaz.toml +4 -0
- binaaz_python_sdk-0.1.0/examples/post_listing.py +48 -0
- binaaz_python_sdk-0.1.0/pyproject.toml +66 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/__init__.py +97 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/_core.py +418 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/_version.py +1 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/client.py +50 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/config.py +83 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/enums.py +73 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/errors.py +107 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/messages.py +60 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/messages.yaml +62 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/models.py +365 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/py.typed +0 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/queries.py +54 -0
- binaaz_python_sdk-0.1.0/src/binaaz_sdk/resources.py +450 -0
- binaaz_python_sdk-0.1.0/tests/__init__.py +0 -0
- binaaz_python_sdk-0.1.0/tests/conftest.py +118 -0
- binaaz_python_sdk-0.1.0/tests/test_async.py +39 -0
- binaaz_python_sdk-0.1.0/tests/test_auth.py +147 -0
- binaaz_python_sdk-0.1.0/tests/test_config.py +60 -0
- binaaz_python_sdk-0.1.0/tests/test_items.py +155 -0
- binaaz_python_sdk-0.1.0/tests/test_photos_catalog.py +74 -0
- binaaz_python_sdk-0.1.0/tests/test_safety.py +113 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Sync `BinaAz` and async `AsyncBinaAz`
|
|
6
|
+
- Sign in with email and password, or with an SMS code through the login service bina.az uses
|
|
7
|
+
- The session is a plain cookie string you can save and pass back; Cloudflare cookies are never kept
|
|
8
|
+
- Create, update, delete, prolong, read and list listings, with cursor pages and a helper that reads every page
|
|
9
|
+
- Listing model with the bina.az rules: 4 to 30 photos, floor not above the building, daily rent only for rentals
|
|
10
|
+
- Refused listings raise `ItemError` with the bina.az status (`RESTRICTED`, `UNVERIFIED`, `INVALID`, `FAILURE`, `LOCKED`)
|
|
11
|
+
- Paid services and paying for a listing need `confirm=True`
|
|
12
|
+
- Photo upload from bytes, paths or files, and rotation
|
|
13
|
+
- Catalog: categories, cities, regions, townships and landmarks
|
|
14
|
+
- Writes are never resent after an ambiguous failure; reads retry with backoff; 429 waits for `Retry-After`
|
|
15
|
+
- The SMS login service is retried when it answers without a known error, and says when more than one SMS may arrive
|
|
16
|
+
- Cloudflare challenge pages raise `BlockedError`
|
|
17
|
+
- goodconf settings, endpoint paths and messages are configurable
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 qiyascc
|
|
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,205 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: binaaz-python-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed Python client for bina.az: sign in by email or SMS code, post, edit, prolong and delete real estate listings, upload photos, read the catalog. Sync and async.
|
|
5
|
+
Project-URL: Homepage, https://theqiyas.cc/projects/binaaz-sdk/
|
|
6
|
+
Project-URL: Source, https://github.com/qiyascc/binaaz-python-sdk
|
|
7
|
+
Project-URL: Author, https://theqiyas.cc
|
|
8
|
+
Project-URL: Issues, https://github.com/qiyascc/binaaz-python-sdk/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/qiyascc/binaaz-python-sdk/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: qiyascc <qiyasturk@gmail.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api,azerbaijan,bina.az,graphql,listings,real estate,sdk
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Office/Business
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Requires-Dist: goodconf>=7.1
|
|
26
|
+
Requires-Dist: httpx<1,>=0.24
|
|
27
|
+
Requires-Dist: pydantic>=2.7
|
|
28
|
+
Requires-Dist: pyyaml>=6
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: goodconf[toml]>=7.1; extra == 'dev'
|
|
31
|
+
Requires-Dist: goodconf[yaml]>=7.1; extra == 'dev'
|
|
32
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
36
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
37
|
+
Provides-Extra: toml
|
|
38
|
+
Requires-Dist: goodconf[toml]>=7.1; extra == 'toml'
|
|
39
|
+
Provides-Extra: yaml
|
|
40
|
+
Requires-Dist: goodconf[yaml]>=7.1; extra == 'yaml'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# binaaz-python-sdk
|
|
44
|
+
|
|
45
|
+
[](https://www.python.org)
|
|
46
|
+
[](https://bina.az)
|
|
47
|
+
[](https://mypy-lang.org)
|
|
48
|
+
[](https://www.python-httpx.org)
|
|
49
|
+
[](https://docs.astral.sh/ruff)
|
|
50
|
+
[](https://github.com/qiyascc/binaaz-python-sdk/blob/main/LICENSE)
|
|
51
|
+
|
|
52
|
+
Typed Python client for bina.az, the Azerbaijani real estate site. Sign in with email and password or with an SMS code, post, edit, prolong and delete listings, upload and rotate photos, read the catalog of cities, districts and metro stations. Sync and async, with retry rules built so a listing or a payment is never sent twice.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install binaaz-python-sdk
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This is not an official bina.az product. It talks to the same endpoints the bina.az website uses, with its own User-Agent. It does not solve captchas or Cloudflare challenges: if bina.az answers with a challenge page you get `BlockedError`.
|
|
59
|
+
|
|
60
|
+
## Sign in
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from binaaz_sdk import BinaAz
|
|
64
|
+
|
|
65
|
+
with BinaAz() as bina:
|
|
66
|
+
bina.auth.login("agency@example.az", "password")
|
|
67
|
+
saved = bina.cookie
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Phone accounts sign in with a code that bina.az sends by SMS:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
sent = bina.auth.send_code("050 123 45 67")
|
|
74
|
+
profile = bina.auth.verify_code("050 123 45 67", "1234")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`bina.cookie` is the signed in session. Keep it somewhere safe and pass it back later, so you do not sign in on every run:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
bina = BinaAz(cookie=saved)
|
|
81
|
+
bina.profile()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A cookie copied from a browser works too, as a header value, a `{"name": "value"}` mapping or a cookie export list. Cloudflare cookies are dropped, they are never stored or sent. Cookies that bina.az renews are picked up from every answer.
|
|
85
|
+
|
|
86
|
+
## Post a listing
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from binaaz_sdk import BinaAz, Category, ContactType, Listing
|
|
90
|
+
|
|
91
|
+
photos = bina.photos.upload_many(["1.jpg", "2.jpg", "3.jpg", "4.jpg"])
|
|
92
|
+
|
|
93
|
+
result = bina.items.create(Listing(
|
|
94
|
+
category_id=Category.APARTMENT_OLD_BUILDING,
|
|
95
|
+
is_leased=False,
|
|
96
|
+
price=120000,
|
|
97
|
+
rooms=3,
|
|
98
|
+
area={"value": 85},
|
|
99
|
+
floor={"value": 4, "total": 9},
|
|
100
|
+
location={"address": "Nizami street 10", "city_id": "1", "latitude": 40.379, "longitude": 49.846},
|
|
101
|
+
contact={"type": ContactType.AGENT, "name": "Agency"},
|
|
102
|
+
photo_ids=[p.id for p in photos],
|
|
103
|
+
description="Renovated three room flat near the metro.",
|
|
104
|
+
))
|
|
105
|
+
result.id, result.url, result.state
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
bina.az takes 4 to 30 photos on a listing. A new listing starts as `pending` and moves to `published`, `rejected` or `awaiting_payment`. When bina.az refuses it, `ItemError.status` tells why: `RESTRICTED` (confirm the phone number first), `UNVERIFIED`, `INVALID` or `FAILURE`. Field problems are in `ItemError.errors`.
|
|
109
|
+
|
|
110
|
+
## Manage listings
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
page = bina.items.list("published", first=50)
|
|
114
|
+
everything = bina.items.all()
|
|
115
|
+
item = bina.items.get("4123456")
|
|
116
|
+
|
|
117
|
+
bina.items.update("4123456", listing)
|
|
118
|
+
bina.items.prolong("4123456")
|
|
119
|
+
bina.items.delete("4123456")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`list` follows bina.az cursors (`page.next_cursor`), `all` reads every page. Tabs are `pending`, `published`, `rejected`, `expired` and `awaiting_payment`. An update of a listing under moderation raises `ItemError` with `status="LOCKED"` and `locked_until`. Listing management needs a company profile (agencies and residences); other profiles get `NotSupportedError`.
|
|
123
|
+
|
|
124
|
+
## Paid services
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
for service in bina.items.paid_services("4123456"):
|
|
128
|
+
service.title, service.options, service.payment_methods
|
|
129
|
+
|
|
130
|
+
payment = bina.items.buy_service("4123456", service.id, "wallet", option_id=service.options[0].id, confirm=True)
|
|
131
|
+
payment.redirect_url
|
|
132
|
+
bina.items.pay("4123456", confirm=True)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Anything that costs money needs `confirm=True`, otherwise `InvalidRequestError` is raised before a request is made. A card payment comes back with `redirect_url`, the page where the payment is finished.
|
|
136
|
+
|
|
137
|
+
## Photos and catalog
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
photo = bina.photos.upload(open("cover.jpg", "rb"))
|
|
141
|
+
photo = bina.photos.rotate(photo.id, 90)
|
|
142
|
+
|
|
143
|
+
bina.catalog.categories()
|
|
144
|
+
bina.catalog.cities()
|
|
145
|
+
bina.catalog.regions("1")
|
|
146
|
+
bina.catalog.townships("4")
|
|
147
|
+
bina.catalog.landmarks("1", 40.379, 49.846)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Uploads take bytes, a path or a file object. Rotating returns a new photo id; use that one in the listing. Catalog calls need no session.
|
|
151
|
+
|
|
152
|
+
## Safety
|
|
153
|
+
|
|
154
|
+
| Situation | What the client does |
|
|
155
|
+
| --- | --- |
|
|
156
|
+
| Connection refused, nothing sent | Retries, then `NotSentError` |
|
|
157
|
+
| Timeout, cut connection or 5xx on create, update, delete, prolong, payment, sign in or photo upload | No retry, `OutcomeUnknownError`. Check the account before trying again |
|
|
158
|
+
| Same failure on a read | Retries with backoff, then `TransportError` or `ServerError` |
|
|
159
|
+
| 429 | Waits `Retry-After` and retries, the request was not processed |
|
|
160
|
+
| SMS login service answers with an error page instead of a known error | Retried up to `code_attempts` (3), the login service drops some answers |
|
|
161
|
+
| SMS code sent only on a retry | `CodeSent.retried` is `True`: more than one SMS may arrive, use the latest code |
|
|
162
|
+
| SMS code rejected after a retry | `CodeRejectedError` says the code may have been used by the earlier attempt |
|
|
163
|
+
| Timeout on the SMS login service | Not resent, `OutcomeUnknownError` |
|
|
164
|
+
| Cloudflare challenge page | `BlockedError`, never solved or retried |
|
|
165
|
+
| Paid service or paying for a listing without `confirm=True` | `InvalidRequestError`, nothing is sent |
|
|
166
|
+
| Fewer than 4 or more than 30 photos | `InvalidRequestError` before anything is sent |
|
|
167
|
+
| A failed sign in | The previous session is kept |
|
|
168
|
+
| Plain http address outside localhost | `ConfigurationError`, unless `allow_insecure_http` |
|
|
169
|
+
|
|
170
|
+
## Endpoints
|
|
171
|
+
|
|
172
|
+
| Namespace | Methods |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `auth` | `login` `send_code` `verify_code` `profile` |
|
|
175
|
+
| `items` | `create` `update` `delete` `prolong` `get` `list` `all` `paid_services` `buy_service` `pay` |
|
|
176
|
+
| `photos` | `upload` `upload_many` `rotate` |
|
|
177
|
+
| `catalog` | `categories` `cities` `regions` `townships` `landmarks` |
|
|
178
|
+
| client | `profile` `cookie` `set_cookie` `clear_session` `graphql` |
|
|
179
|
+
|
|
180
|
+
`AsyncBinaAz` has the same methods, awaited. `graphql(query, variables)` sends any other query with the session.
|
|
181
|
+
|
|
182
|
+
## Errors
|
|
183
|
+
|
|
184
|
+
`BinaAzError` is the base. Answers raise `APIError` subclasses: `BadRequestError`, `AuthenticationError` (the session is not signed in), `NotFoundError`, `RateLimitError`, `ServerError` and `BlockedError`. GraphQL errors raise `GraphQLError` with `errors`, refused listings raise `ItemError` with `status`, `errors` and `locked_until`, a wrong SMS code raises `CodeRejectedError`. Each carries `status_code`, `message` and the raw `body`.
|
|
185
|
+
|
|
186
|
+
## Config
|
|
187
|
+
|
|
188
|
+
Pass values in code, or set `BINAAZ_COOKIE`, `BINAAZ_TIMEOUT`, `BINAAZ_MAX_RETRIES`, `BINAAZ_CODE_ATTEMPTS`, `BINAAZ_USER_AGENT` and the rest, or put them in `binaaz.toml` ([example](https://github.com/qiyascc/binaaz-python-sdk/blob/main/examples/binaaz.toml)). Every path is a setting (`BINAAZ_ENDPOINTS__GRAPHQL=...`). Messages live in `messages.yaml` and can be replaced with `messages.use(...)`.
|
|
189
|
+
|
|
190
|
+
## Develop
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
pip install -e ".[dev]" && pytest && ruff check . && mypy src
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Links
|
|
197
|
+
|
|
198
|
+
- Source code: [github.com/qiyascc/binaaz-python-sdk](https://github.com/qiyascc/binaaz-python-sdk)
|
|
199
|
+
- PyPI: [pypi.org/project/binaaz-python-sdk](https://pypi.org/project/binaaz-python-sdk/)
|
|
200
|
+
- Project page: [theqiyas.cc/projects/binaaz-sdk](https://theqiyas.cc/projects/binaaz-sdk/)
|
|
201
|
+
- Author: [Qiyas Sadıqov](https://theqiyas.cc), more projects at [theqiyas.cc](https://theqiyas.cc)
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# binaaz-python-sdk
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org)
|
|
4
|
+
[](https://bina.az)
|
|
5
|
+
[](https://mypy-lang.org)
|
|
6
|
+
[](https://www.python-httpx.org)
|
|
7
|
+
[](https://docs.astral.sh/ruff)
|
|
8
|
+
[](https://github.com/qiyascc/binaaz-python-sdk/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
Typed Python client for bina.az, the Azerbaijani real estate site. Sign in with email and password or with an SMS code, post, edit, prolong and delete listings, upload and rotate photos, read the catalog of cities, districts and metro stations. Sync and async, with retry rules built so a listing or a payment is never sent twice.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install binaaz-python-sdk
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This is not an official bina.az product. It talks to the same endpoints the bina.az website uses, with its own User-Agent. It does not solve captchas or Cloudflare challenges: if bina.az answers with a challenge page you get `BlockedError`.
|
|
17
|
+
|
|
18
|
+
## Sign in
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from binaaz_sdk import BinaAz
|
|
22
|
+
|
|
23
|
+
with BinaAz() as bina:
|
|
24
|
+
bina.auth.login("agency@example.az", "password")
|
|
25
|
+
saved = bina.cookie
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Phone accounts sign in with a code that bina.az sends by SMS:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
sent = bina.auth.send_code("050 123 45 67")
|
|
32
|
+
profile = bina.auth.verify_code("050 123 45 67", "1234")
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`bina.cookie` is the signed in session. Keep it somewhere safe and pass it back later, so you do not sign in on every run:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
bina = BinaAz(cookie=saved)
|
|
39
|
+
bina.profile()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
A cookie copied from a browser works too, as a header value, a `{"name": "value"}` mapping or a cookie export list. Cloudflare cookies are dropped, they are never stored or sent. Cookies that bina.az renews are picked up from every answer.
|
|
43
|
+
|
|
44
|
+
## Post a listing
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from binaaz_sdk import BinaAz, Category, ContactType, Listing
|
|
48
|
+
|
|
49
|
+
photos = bina.photos.upload_many(["1.jpg", "2.jpg", "3.jpg", "4.jpg"])
|
|
50
|
+
|
|
51
|
+
result = bina.items.create(Listing(
|
|
52
|
+
category_id=Category.APARTMENT_OLD_BUILDING,
|
|
53
|
+
is_leased=False,
|
|
54
|
+
price=120000,
|
|
55
|
+
rooms=3,
|
|
56
|
+
area={"value": 85},
|
|
57
|
+
floor={"value": 4, "total": 9},
|
|
58
|
+
location={"address": "Nizami street 10", "city_id": "1", "latitude": 40.379, "longitude": 49.846},
|
|
59
|
+
contact={"type": ContactType.AGENT, "name": "Agency"},
|
|
60
|
+
photo_ids=[p.id for p in photos],
|
|
61
|
+
description="Renovated three room flat near the metro.",
|
|
62
|
+
))
|
|
63
|
+
result.id, result.url, result.state
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
bina.az takes 4 to 30 photos on a listing. A new listing starts as `pending` and moves to `published`, `rejected` or `awaiting_payment`. When bina.az refuses it, `ItemError.status` tells why: `RESTRICTED` (confirm the phone number first), `UNVERIFIED`, `INVALID` or `FAILURE`. Field problems are in `ItemError.errors`.
|
|
67
|
+
|
|
68
|
+
## Manage listings
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
page = bina.items.list("published", first=50)
|
|
72
|
+
everything = bina.items.all()
|
|
73
|
+
item = bina.items.get("4123456")
|
|
74
|
+
|
|
75
|
+
bina.items.update("4123456", listing)
|
|
76
|
+
bina.items.prolong("4123456")
|
|
77
|
+
bina.items.delete("4123456")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`list` follows bina.az cursors (`page.next_cursor`), `all` reads every page. Tabs are `pending`, `published`, `rejected`, `expired` and `awaiting_payment`. An update of a listing under moderation raises `ItemError` with `status="LOCKED"` and `locked_until`. Listing management needs a company profile (agencies and residences); other profiles get `NotSupportedError`.
|
|
81
|
+
|
|
82
|
+
## Paid services
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
for service in bina.items.paid_services("4123456"):
|
|
86
|
+
service.title, service.options, service.payment_methods
|
|
87
|
+
|
|
88
|
+
payment = bina.items.buy_service("4123456", service.id, "wallet", option_id=service.options[0].id, confirm=True)
|
|
89
|
+
payment.redirect_url
|
|
90
|
+
bina.items.pay("4123456", confirm=True)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Anything that costs money needs `confirm=True`, otherwise `InvalidRequestError` is raised before a request is made. A card payment comes back with `redirect_url`, the page where the payment is finished.
|
|
94
|
+
|
|
95
|
+
## Photos and catalog
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
photo = bina.photos.upload(open("cover.jpg", "rb"))
|
|
99
|
+
photo = bina.photos.rotate(photo.id, 90)
|
|
100
|
+
|
|
101
|
+
bina.catalog.categories()
|
|
102
|
+
bina.catalog.cities()
|
|
103
|
+
bina.catalog.regions("1")
|
|
104
|
+
bina.catalog.townships("4")
|
|
105
|
+
bina.catalog.landmarks("1", 40.379, 49.846)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Uploads take bytes, a path or a file object. Rotating returns a new photo id; use that one in the listing. Catalog calls need no session.
|
|
109
|
+
|
|
110
|
+
## Safety
|
|
111
|
+
|
|
112
|
+
| Situation | What the client does |
|
|
113
|
+
| --- | --- |
|
|
114
|
+
| Connection refused, nothing sent | Retries, then `NotSentError` |
|
|
115
|
+
| Timeout, cut connection or 5xx on create, update, delete, prolong, payment, sign in or photo upload | No retry, `OutcomeUnknownError`. Check the account before trying again |
|
|
116
|
+
| Same failure on a read | Retries with backoff, then `TransportError` or `ServerError` |
|
|
117
|
+
| 429 | Waits `Retry-After` and retries, the request was not processed |
|
|
118
|
+
| SMS login service answers with an error page instead of a known error | Retried up to `code_attempts` (3), the login service drops some answers |
|
|
119
|
+
| SMS code sent only on a retry | `CodeSent.retried` is `True`: more than one SMS may arrive, use the latest code |
|
|
120
|
+
| SMS code rejected after a retry | `CodeRejectedError` says the code may have been used by the earlier attempt |
|
|
121
|
+
| Timeout on the SMS login service | Not resent, `OutcomeUnknownError` |
|
|
122
|
+
| Cloudflare challenge page | `BlockedError`, never solved or retried |
|
|
123
|
+
| Paid service or paying for a listing without `confirm=True` | `InvalidRequestError`, nothing is sent |
|
|
124
|
+
| Fewer than 4 or more than 30 photos | `InvalidRequestError` before anything is sent |
|
|
125
|
+
| A failed sign in | The previous session is kept |
|
|
126
|
+
| Plain http address outside localhost | `ConfigurationError`, unless `allow_insecure_http` |
|
|
127
|
+
|
|
128
|
+
## Endpoints
|
|
129
|
+
|
|
130
|
+
| Namespace | Methods |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `auth` | `login` `send_code` `verify_code` `profile` |
|
|
133
|
+
| `items` | `create` `update` `delete` `prolong` `get` `list` `all` `paid_services` `buy_service` `pay` |
|
|
134
|
+
| `photos` | `upload` `upload_many` `rotate` |
|
|
135
|
+
| `catalog` | `categories` `cities` `regions` `townships` `landmarks` |
|
|
136
|
+
| client | `profile` `cookie` `set_cookie` `clear_session` `graphql` |
|
|
137
|
+
|
|
138
|
+
`AsyncBinaAz` has the same methods, awaited. `graphql(query, variables)` sends any other query with the session.
|
|
139
|
+
|
|
140
|
+
## Errors
|
|
141
|
+
|
|
142
|
+
`BinaAzError` is the base. Answers raise `APIError` subclasses: `BadRequestError`, `AuthenticationError` (the session is not signed in), `NotFoundError`, `RateLimitError`, `ServerError` and `BlockedError`. GraphQL errors raise `GraphQLError` with `errors`, refused listings raise `ItemError` with `status`, `errors` and `locked_until`, a wrong SMS code raises `CodeRejectedError`. Each carries `status_code`, `message` and the raw `body`.
|
|
143
|
+
|
|
144
|
+
## Config
|
|
145
|
+
|
|
146
|
+
Pass values in code, or set `BINAAZ_COOKIE`, `BINAAZ_TIMEOUT`, `BINAAZ_MAX_RETRIES`, `BINAAZ_CODE_ATTEMPTS`, `BINAAZ_USER_AGENT` and the rest, or put them in `binaaz.toml` ([example](https://github.com/qiyascc/binaaz-python-sdk/blob/main/examples/binaaz.toml)). Every path is a setting (`BINAAZ_ENDPOINTS__GRAPHQL=...`). Messages live in `messages.yaml` and can be replaced with `messages.use(...)`.
|
|
147
|
+
|
|
148
|
+
## Develop
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pip install -e ".[dev]" && pytest && ruff check . && mypy src
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Links
|
|
155
|
+
|
|
156
|
+
- Source code: [github.com/qiyascc/binaaz-python-sdk](https://github.com/qiyascc/binaaz-python-sdk)
|
|
157
|
+
- PyPI: [pypi.org/project/binaaz-python-sdk](https://pypi.org/project/binaaz-python-sdk/)
|
|
158
|
+
- Project page: [theqiyas.cc/projects/binaaz-sdk](https://theqiyas.cc/projects/binaaz-sdk/)
|
|
159
|
+
- Author: [Qiyas Sadıqov](https://theqiyas.cc), more projects at [theqiyas.cc](https://theqiyas.cc)
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from binaaz_sdk import BinaAz, Category, ContactType, ItemError, Listing
|
|
8
|
+
|
|
9
|
+
SESSION = Path("binaaz.session")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def signed_in(bina: BinaAz) -> None:
|
|
13
|
+
if SESSION.exists():
|
|
14
|
+
bina.set_cookie(SESSION.read_text())
|
|
15
|
+
return
|
|
16
|
+
phone = os.environ["BINAAZ_PHONE"]
|
|
17
|
+
sent = bina.auth.send_code(phone)
|
|
18
|
+
if sent.retried:
|
|
19
|
+
print("More than one SMS may arrive, use the latest code")
|
|
20
|
+
bina.auth.verify_code(phone, input("SMS code: "))
|
|
21
|
+
SESSION.write_text(bina.cookie)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(folder: str) -> None:
|
|
25
|
+
with BinaAz() as bina:
|
|
26
|
+
signed_in(bina)
|
|
27
|
+
photos = bina.photos.upload_many(sorted(Path(folder).glob("*.jpg"))[:30])
|
|
28
|
+
listing = Listing(
|
|
29
|
+
category_id=Category.APARTMENT_NEW_BUILDING,
|
|
30
|
+
is_leased=False,
|
|
31
|
+
price=150000,
|
|
32
|
+
rooms=2,
|
|
33
|
+
area={"value": 70},
|
|
34
|
+
floor={"value": 8, "total": 16},
|
|
35
|
+
location={"address": "Yasamal", "city_id": "1", "latitude": 40.393, "longitude": 49.808},
|
|
36
|
+
contact={"type": ContactType.OWNER, "name": "Owner"},
|
|
37
|
+
photo_ids=[p.id for p in photos],
|
|
38
|
+
)
|
|
39
|
+
try:
|
|
40
|
+
result = bina.items.create(listing)
|
|
41
|
+
except ItemError as exc:
|
|
42
|
+
print(exc.status, exc.errors)
|
|
43
|
+
raise
|
|
44
|
+
print(result.id, result.state, result.url)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if __name__ == "__main__":
|
|
48
|
+
main(sys.argv[1] if len(sys.argv) > 1 else ".")
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "binaaz-python-sdk"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Typed Python client for bina.az: sign in by email or SMS code, post, edit, prolong and delete real estate listings, upload photos, read the catalog. Sync and async."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "qiyascc", email = "qiyasturk@gmail.com" }]
|
|
14
|
+
keywords = ["bina.az", "real estate", "azerbaijan", "listings", "graphql", "sdk", "api"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Office/Business",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["httpx>=0.24,<1", "goodconf>=7.1", "pydantic>=2.7", "PyYAML>=6"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
toml = ["goodconf[toml]>=7.1"]
|
|
31
|
+
yaml = ["goodconf[yaml]>=7.1"]
|
|
32
|
+
dev = ["binaaz-python-sdk[toml,yaml]", "pytest>=7", "pytest-asyncio>=0.21", "ruff>=0.4", "mypy>=1.8", "types-PyYAML"]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://theqiyas.cc/projects/binaaz-sdk/"
|
|
36
|
+
Source = "https://github.com/qiyascc/binaaz-python-sdk"
|
|
37
|
+
Author = "https://theqiyas.cc"
|
|
38
|
+
Issues = "https://github.com/qiyascc/binaaz-python-sdk/issues"
|
|
39
|
+
Changelog = "https://github.com/qiyascc/binaaz-python-sdk/blob/main/CHANGELOG.md"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.version]
|
|
42
|
+
path = "src/binaaz_sdk/_version.py"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/binaaz_sdk"]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.sdist]
|
|
48
|
+
include = ["src/binaaz_sdk", "tests", "examples", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
testpaths = ["tests"]
|
|
52
|
+
asyncio_mode = "auto"
|
|
53
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
line-length = 120
|
|
57
|
+
target-version = "py310"
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
61
|
+
ignore = ["E731", "UP006", "UP007", "UP035", "UP045"]
|
|
62
|
+
|
|
63
|
+
[tool.mypy]
|
|
64
|
+
python_version = "3.10"
|
|
65
|
+
plugins = ["pydantic.mypy"]
|
|
66
|
+
warn_unused_ignores = true
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from . import messages
|
|
2
|
+
from ._version import __version__
|
|
3
|
+
from .client import AsyncBinaAz, BinaAz
|
|
4
|
+
from .config import Endpoints, Settings, load_settings
|
|
5
|
+
from .enums import PHOTO_ANGLES, PHOTOS_MAX, PHOTOS_MIN, BuildingType, Category, ContactType, ItemState, ItemTab
|
|
6
|
+
from .errors import (
|
|
7
|
+
APIError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
BadRequestError,
|
|
10
|
+
BinaAzError,
|
|
11
|
+
BlockedError,
|
|
12
|
+
CodeRejectedError,
|
|
13
|
+
ConfigurationError,
|
|
14
|
+
GraphQLError,
|
|
15
|
+
InvalidRequestError,
|
|
16
|
+
ItemError,
|
|
17
|
+
NotFoundError,
|
|
18
|
+
NotSentError,
|
|
19
|
+
NotSupportedError,
|
|
20
|
+
OutcomeUnknownError,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
ServerError,
|
|
23
|
+
TransportError,
|
|
24
|
+
)
|
|
25
|
+
from .models import (
|
|
26
|
+
Area,
|
|
27
|
+
CategoryNode,
|
|
28
|
+
CodeSent,
|
|
29
|
+
Contact,
|
|
30
|
+
Floor,
|
|
31
|
+
Item,
|
|
32
|
+
ItemPage,
|
|
33
|
+
ItemResult,
|
|
34
|
+
Listing,
|
|
35
|
+
Location,
|
|
36
|
+
PaidService,
|
|
37
|
+
Payment,
|
|
38
|
+
PaymentMethod,
|
|
39
|
+
Photo,
|
|
40
|
+
Place,
|
|
41
|
+
Price,
|
|
42
|
+
Profile,
|
|
43
|
+
ServiceOption,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"__version__",
|
|
48
|
+
"BinaAz",
|
|
49
|
+
"AsyncBinaAz",
|
|
50
|
+
"Settings",
|
|
51
|
+
"Endpoints",
|
|
52
|
+
"load_settings",
|
|
53
|
+
"messages",
|
|
54
|
+
"Category",
|
|
55
|
+
"ContactType",
|
|
56
|
+
"BuildingType",
|
|
57
|
+
"ItemState",
|
|
58
|
+
"ItemTab",
|
|
59
|
+
"PHOTO_ANGLES",
|
|
60
|
+
"PHOTOS_MIN",
|
|
61
|
+
"PHOTOS_MAX",
|
|
62
|
+
"Listing",
|
|
63
|
+
"Area",
|
|
64
|
+
"Floor",
|
|
65
|
+
"Location",
|
|
66
|
+
"Contact",
|
|
67
|
+
"Profile",
|
|
68
|
+
"Item",
|
|
69
|
+
"ItemPage",
|
|
70
|
+
"ItemResult",
|
|
71
|
+
"Price",
|
|
72
|
+
"Photo",
|
|
73
|
+
"Place",
|
|
74
|
+
"CategoryNode",
|
|
75
|
+
"PaidService",
|
|
76
|
+
"ServiceOption",
|
|
77
|
+
"PaymentMethod",
|
|
78
|
+
"Payment",
|
|
79
|
+
"CodeSent",
|
|
80
|
+
"BinaAzError",
|
|
81
|
+
"ConfigurationError",
|
|
82
|
+
"InvalidRequestError",
|
|
83
|
+
"TransportError",
|
|
84
|
+
"NotSentError",
|
|
85
|
+
"OutcomeUnknownError",
|
|
86
|
+
"APIError",
|
|
87
|
+
"BlockedError",
|
|
88
|
+
"BadRequestError",
|
|
89
|
+
"AuthenticationError",
|
|
90
|
+
"NotFoundError",
|
|
91
|
+
"RateLimitError",
|
|
92
|
+
"ServerError",
|
|
93
|
+
"GraphQLError",
|
|
94
|
+
"ItemError",
|
|
95
|
+
"NotSupportedError",
|
|
96
|
+
"CodeRejectedError",
|
|
97
|
+
]
|