lalafo-python-sdk
Typed Python client for lalafo.az. Sign in, read categories and their params, post an ad with photos, edit, deactivate, activate and delete it, and list your own ads. Sync and async, with retry rules built so an ad is never posted twice.
pip install lalafo-python-sdk
The client talks to the same https://lalafo.az/api/... endpoints as the lalafo.az website, with the same headers (device: pc, country-id, language, a fresh request-id, user-hash). It sends no device fingerprint and no mobile app identity. lalafo has no public API and can change these endpoints; every path is a setting.
Sign in
from lalafo_sdk import Lalafo
with Lalafo() as lf:
session = lf.login("050 123 45 67", "password")
lf.auth.me().username
Sign in with a phone number (050 123 45 67, +994501234567) or an email. Keep the session instead of signing in every time:
import json
def save(session):
open("session.json", "w").write(json.dumps(session.to_dict()))
lf = Lalafo(json.load(open("session.json")), on_session=save)
The token is renewed with the refresh token once 75% of its lifetime has passed (renew_at), or once after a 401. on_session gets every new session, so store it there. A renewal that lalafo refuses raises AuthenticationError: sign in again.
Post an ad
from lalafo_sdk import BAKU
rooms = next(p for p in lf.catalog.filter_params(4800, BAKU) if p.id == 69)
form = {
"category_id": 4800,
"city_id": BAKU,
"price": 80000,
"currency": "AZN",
"description": "Satılır 2 otaqlı köhnə tikili, təmirli, kupçalı.",
"params": [
{"id": 69, "value_id": rooms.find("2 otaqlı").id},
{"id": 70, "value": "42"},
{"id": 1239, "value_id": 28272},
{"id": 1239, "value_id": 27535},
],
}
ad = lf.ads.create(form, ["kitchen.jpg", "room.jpg"])
ad.id, ad.status, ad.url
create does what the website does: it loads the posting params of the category and city, makes a draft (and clears photos left in a reused draft), uploads the photos, saves the draft and publishes it. lalafo writes the title itself, such as "Bakı, 2 otaqlı, Köhnə tikili, 42 kv. m". The ad goes to moderation (AdStatus.MODERATION); lf.ads.wait(ad.id) waits until moderation ends. An ad over the free limit comes back as AdStatus.PAYMENT_WAITING (ad.awaiting_payment): lalafo shows it only after payment on the website. The client never pays.
Params are checked before the draft is made: an unknown param, an unknown value_id, two values for a single value param or a missing required param raise InvalidRequestError and nothing is sent. Dropdowns take value_id, inputs take value, a multi select param repeats with each value_id. Leave price out for a negotiable price. Photos are bytes, paths or (filename, bytes, content_type), from 1 to 20, each from 20 KB to 20 MB.
Each step is also there on its own: ads.draft(), ads.upload_photo(draft_id, photo), ads.save_draft(draft_id, form, image_ids), ads.publish(draft_id).
Manage ads
lf.ads.edit(ad.id, {**form, "price": 75000}, ["new.jpg"])
lf.ads.edit(ad.id, form, ["only.jpg"], keep_photos=False)
lf.ads.deactivate(ad.id)
lf.ads.activate(ad.id)
lf.ads.delete(ad.id)
lf.ads.get(ad.id).rejected_reason
lf.ads.details(ad.id)
page = lf.ads.list("active", page=1, per_page=20)
everything = lf.ads.all("deactivated")
lf.ads.limits().raw
list takes active, moderation, rejected, deactivated or payment_waiting. An edit sends the ad back to moderation.
Categories, cities and params
estate = lf.catalog.real_estate()
for path, category in estate.walk():
if category.postable:
print(category.id, " / ".join(path))
lf.catalog.cities()
lf.catalog.filter_params(4800, BAKU)
lf.catalog.posting_params(4800, BAKU)
filter_params is public. posting_params needs a session and is the exact list the post form uses. Real estate ids on lalafo.az: apartments 4800 sale old building, 4793 sale new building, 2044 long term rent, 2045 daily rent; houses 2031 sale, 2033 rent; land 2038 sale, 2039 rent; country houses 5952, 5953; rooms 2050, 2052, 2053; garages 5304, 5305; commercial rent 2065 to 2072. Bakı is city 103261.
Safety
| Situation | What the client does |
|---|---|
| Connection refused, nothing sent | Retries, then NotSentError |
| Cloudflare check page | Retries, the request never reached lalafo, then BlockedError |
| Timeout, cut connection or 5xx on a write (sign in, draft, upload, save, publish, edit, deactivate, activate, delete) | No retry, OutcomeUnknownError: check the account before trying again |
| Same failure on a read | Retries with backoff, then TransportError or ServerError |
| 429 | Waits Retry-After and retries |
| 401 with a refresh token | Renews the session once and repeats the call, the call was not processed |
| Captcha asked after failed sign ins | CaptchaRequiredError, the client never solves captchas |
| Bad params, photos, phone or form | InvalidRequestError before anything is sent |
base_url over plain http |
ConfigurationError, except localhost or allow_insecure_http |
lalafo.az is sometimes slow and sometimes answers with a short Cloudflare check page. Reads ride over that with retries. A write that timed out may still have gone through, so it is never sent again on its own: look at lf.ads.list() first.
Endpoints
| Namespace | Methods |
|---|---|
auth |
login renew logout me |
catalog |
categories category real_estate cities filter_params posting_params |
ads |
create draft upload_photo remove_photos save_draft publish edit deactivate activate delete get details list all limits wait |
| client | login use_session request |
AsyncLalafo has the same methods, awaited.
Errors
LalafoError is the base. API answers raise APIError subclasses by status: ValidationError (400, 422, with .fields), CaptchaRequiredError, AuthenticationError (401), ForbiddenError (403), NotFoundError (404), ConflictError (409), RateLimitError (429), ServerError (5xx), BadRequestError for the rest. Each carries status_code, message and the raw body. SessionRequiredError means a call needs a signed in client.
Config
Pass values in code, or set LALAFO_BASE_URL, LALAFO_COUNTRY_ID, LALAFO_LANGUAGE, LALAFO_PHONE_CODE, LALAFO_TIMEOUT, LALAFO_MAX_RETRIES, LALAFO_RENEW_AT, or put them in lalafo.toml (example). Every path is a setting (LALAFO_ENDPOINTS__LOGIN=...). Messages live in messages.yaml and can be replaced with messages.use(...).
The defaults are lalafo.az (country 13, az_AZ, +994), the only site this client was built and tested against. lalafo runs the same platform in other countries; point base_url, country_id, language and phone_code at one of them at your own risk.
Develop
pip install -e ".[dev]" && pytest && ruff check . && mypy src
Links
- Source code: github.com/qiyascc/lalafo-python-sdk
- PyPI: pypi.org/project/lalafo-python-sdk
- Project page: theqiyas.cc/projects/lalafo-sdk
- Author: Qiyas Sadıqov, more projects at theqiyas.cc
License
MIT
Metadata
Release files for lalafo-python-sdk 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| lalafo_python_sdk-0.1.0.tar.gz | 29.2 kB | Without provenance | Details for lalafo_python_sdk-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| lalafo_python_sdk-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for lalafo_python_sdk-0.1.0-py3-none-any.whl |
Total release size: 54.6 kB
Release files / lalafo_python_sdk-0.1.0.tar.gz
| Download URL | lalafo_python_sdk-0.1.0.tar.gz |
|---|---|
| Size | 29.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3d7f3c48b9df59e0f71514a1fa5668e5aebea2a93dbb075da71b179ac8f6acb8
|
|
BLAKE2b-256 checksum How to use checksums |
4a4c310b606ffa9d429ae2444b9cc8d27035f16cfd6f3af5adb127e45cb8b4b2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / lalafo_python_sdk-0.1.0-py3-none-any.whl
| Download URL | lalafo_python_sdk-0.1.0-py3-none-any.whl |
|---|---|
| Size | 25.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5f256f3ad267f072b5e21d88c6bce77aff5415cc3ead7c0165f121aea1f68453
|
|
BLAKE2b-256 checksum How to use checksums |
3007f22805316d068b76a6800b9e8ce299385ec37e13b5a0b8c0b53ff3ad9b0f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Security for lalafo-python-sdk 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.