Metadata-Version: 2.4
Name: unipile-python-community
Version: 0.1.0
Summary: Unofficial python client for the Unipile API - messaging (LinkedIn, WhatsApp, Instagram, ...) and email. Not affiliated with Unipile.
Project-URL: Homepage, https://github.com/DevByAli/unipile-python-community
Project-URL: API docs (Unipile), https://developer.unipile.com/docs
Project-URL: Source, https://github.com/DevByAli/unipile-python-community
Author: DevByAli
License-Expression: MIT
License-File: LICENSE
Keywords: api,email,instagram,linkedin,messaging,sdk,unipile,whatsapp
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: pydantic>=2
Requires-Dist: requests>=2.25
Provides-Extra: qrcode
Requires-Dist: qrcode>=7; extra == 'qrcode'
Description-Content-Type: text/markdown

# Unipile Python Community SDK

> **Unofficial.** A community port, not affiliated with, endorsed by, or maintained by Unipile.
> For the vendor-supported client see [unipile-node-sdk](https://github.com/unipile/unipile-node-sdk).
> The MIT LICENSE keeps Unipile's copyright, since this is a derivative of their SDK.

Python port of [unipile-node-sdk](https://github.com/unipile/unipile-node-sdk). Same endpoints,
same resource layout (`client.account`, `client.messaging`, `client.users`, `client.email`,
`client.webhook`), python naming and kwargs instead of input objects.

## Install

```bash
pip install unipile-python-community

# add [qrcode] to draw the WhatsApp/Telegram QR codes (see qr_code_string below)
pip install "unipile-python-community[qrcode]"
```

## Quick start

```python
from unipile import UnipileClient

client = UnipileClient("https://api1.unipile.com:13111", "YOUR_ACCESS_TOKEN")

client.account.connect("LINKEDIN", username="your@email.com", password="password")
client.users.get_company_profile("unipile", account_id="t5XY4yQzR9WVrlNFyzPMhw")
```

## Typed responses

Every method returns a typed model, so your editor completes the fields and mypy/pyright check
them. Attachments come back as `bytes`, and a non 2xx answer raises `UnsuccessfulRequestError`.

```python
chats = client.messaging.get_all_chats(limit=10)     # Paginated[Chat]
for chat in chats.items:
    print(chat.name, chat.unread_count, chat.last_message.text if chat.last_message else "")
next_page = client.messaging.get_all_chats(cursor=chats.cursor)

profile = client.users.get_profile("john-doe", account_id)   # UserProfile
print(profile.headline, profile.work_experience[0].company, profile.skills[0].name)
```

Two things keep these models from going stale:

- **unknown fields are kept, not rejected.** A field the API adds tomorrow arrives as an
  attribute (`chat.brand_new_field`) instead of raising.
- **anything provider- or endpoint-specific is optional.** A LinkedIn account and an IMAP
  account parse with the same `Account`; what the provider doesn't send is `None`.

Import the models for your own annotations, and drop to plain dicts any time:

```python
from unipile import Chat, Email, Paginated, UserProfile     # also unipile.models.Chat

def summarize(page: Paginated[Chat]) -> str: ...

raw = client.request("GET", ["chats"])   # no model= -> the dict exactly as the API sent it
```

## Accounts

```python
client.account.get_all(limit=10)
client.account.get_one("t5XY4yQzR9WVrlNFyzPMhw")
client.account.delete("t5XY4yQzR9WVrlNFyzPMhw")

client.account.create_hosted_auth_link(
    type="create",
    expires_on="2025-12-31T23:59:59.999Z",
    api_url="https://api1.unipile.com:13111",
    providers="*",
)

client.account.connect("LINKEDIN", username="your@email.com", password="password")
client.account.connect("LINKEDIN", access_token="li_at cookie value")
client.account.connect("INSTAGRAM", username="username", password="password")
client.account.connect("MESSENGER", username="username", password="password")
client.account.connect("TWITTER", username="username", email="your@email.com", password="password")
client.account.connect("GOOGLE_OAUTH", refresh_token="...", access_token="...")

# WhatsApp and Telegram answer with a code to render as a QR and scan
response = client.account.connect("WHATSAPP")
print(response.checkpoint.qrcode)   # the raw code
print(response.qr_code_string)      # the same code drawn as a scannable QR

client.account.solve_code_checkpoint("LINKEDIN", "t5XY4yQzR9WVrlNFyzPMhw", "123456")
client.account.resync_linkedin_account("t5XY4yQzR9WVrlNFyzPMhw", linkedin_product="recruiter")
```

`reconnect(account_id, provider, **fields)` takes the same fields for an account that dropped.
`connect`/`reconnect` pass every keyword straight through, so any provider and any field the API
adds works without an SDK update.

## Messaging

```python
client.messaging.get_all_chats(limit=10, unread=True, account_type="LINKEDIN")
client.messaging.get_chat("vISKyHtDUmagrk6vrnlXhw")
client.messaging.get_all_messages_from_chat("vISKyHtDUmagrk6vrnlXhw", limit=50)

client.messaging.send_message("vISKyHtDUmagrk6vrnlXhw", "Hello")

with open("cat.png", "rb") as file:
    client.messaging.send_message(
        "vISKyHtDUmagrk6vrnlXhw", "Look", attachments=[("cat.png", file.read())]
    )

client.messaging.start_new_chat(
    "t5XY4yQzR9WVrlNFyzPMhw", "Hello", ["ACoAAA..."],
    linkedin={"api": "classic", "inmail": True},   # or {"api": "recruiter", "signature": "..."}
)

client.messaging.set_chat_status("vISKyHtDUmagrk6vrnlXhw", True)   # mark as read

data = client.messaging.get_message_attachment("msg_id", "attachment_id")   # bytes
```

Attendees: `get_all_attendees()`, `get_attendee(id)`, `get_all_attendees_from_chat(chat_id)`,
`get_all_messages_from_attendee(id)`, `get_all_chats_from_attendee(id)`.

## Users & posts (LinkedIn)

```python
client.users.get_profile("john-doe", "t5XY4yQzR9WVrlNFyzPMhw", linkedin_sections="*")
client.users.get_own_profile("t5XY4yQzR9WVrlNFyzPMhw")
client.users.get_all_relations("t5XY4yQzR9WVrlNFyzPMhw", limit=50)

client.users.send_invitation("t5XY4yQzR9WVrlNFyzPMhw", "ACoAAA...", message="Hi!")
client.users.get_all_invitations_sent("t5XY4yQzR9WVrlNFyzPMhw")
client.users.cancel_invitation_sent("t5XY4yQzR9WVrlNFyzPMhw", "invitation_id")

client.users.get_all_posts("john-doe", "t5XY4yQzR9WVrlNFyzPMhw", is_company=False)
client.users.get_post("post_id", "t5XY4yQzR9WVrlNFyzPMhw")
client.users.create_post("t5XY4yQzR9WVrlNFyzPMhw", "Hello world")
client.users.send_post_comment("post_id", "t5XY4yQzR9WVrlNFyzPMhw", "Nice")
client.users.get_all_post_comments("post_id", "t5XY4yQzR9WVrlNFyzPMhw")
client.users.send_post_reaction("t5XY4yQzR9WVrlNFyzPMhw", "post_id", reaction_type="like")
```

## Email

```python
client.email.get_all(account_id="t5XY4yQzR9WVrlNFyzPMhw", limit=20)   # Paginated[Email]
client.email.get_one("TnOWcaycS52dwnhgADlb2w")
client.email.get_one_by_provider_id("provider_id", "t5XY4yQzR9WVrlNFyzPMhw")
client.email.update("TnOWcaycS52dwnhgADlb2w", unread=False)
client.email.delete("TnOWcaycS52dwnhgADlb2w")

client.email.send(
    "t5XY4yQzR9WVrlNFyzPMhw",
    [{"identifier": "someone@company.com", "display_name": "Someone"}],
    "<p>Hello</p>",
    subject="Hello",
    cc=[{"identifier": "other@company.com"}],
    attachments=[("report.pdf", pdf_bytes)],
)

client.email.send(                                    # reply
    "t5XY4yQzR9WVrlNFyzPMhw", [{"identifier": "someone@company.com"}], "<p>Sure</p>",
    reply_to="email_provider_id",
)

client.email.get_all_folders(account_id="t5XY4yQzR9WVrlNFyzPMhw")
client.email.get_one_folder("aG0z55cmQOO1y2180eAeuQ")
data = client.email.get_email_attachment("email_id", "attachment_id")   # bytes
```

The node SDK's `email.getOne.byProviderId(...)` sub methods are plain methods here:
`get_one_by_provider_id`, `delete_by_provider_id`, `update_by_provider_id`,
`get_one_folder_by_provider_id`, `get_email_attachment_by_provider_id`.

## Webhooks

```python
client.webhook.get_all()
client.webhook.create("https://mine.dev/hook", "messaging", events=["message_received"])
client.webhook.delete("webhook_id")
```

## Errors

```python
from unipile import UnipileError, UnsuccessfulRequestError

try:
    client.messaging.get_all_attendees(account_id="bad_id")
except UnsuccessfulRequestError as error:
    print(error.status, error.body)   # 422, {'title': 'errors/invalid_credentials', ...}
except UnipileError as error:
    print(error)                      # InvalidBaseUrlError, InvalidTokenError, InvalidInputTypeError
```

Validation is client side only where a bad argument can't produce a valid request: the base_url and
token at instantiation, and empty or missing ids before a call goes out. Responses are parsed into
the models above, which accept unknown fields rather than rejecting them.

## Extra parameters and unpackaged endpoints

Any unknown keyword goes through untouched, to the query string or the body depending on the call:

```python
client.messaging.get_message_attachment("msg_id", "att_id", some_new_param="value")
```

For an endpoint no method covers yet:

```python
client.request("GET", ["chats", "some_id", "messages"], params={"limit": "10"})
client.request("POST", ["some", "new", "endpoint"], json={"key": "value"})
```

## Client options

```python
client = UnipileClient(
    "https://api1.unipile.com:13111", "token",
    api_version="v1",       # default
    timeout=30.0,           # seconds, default
    session=None,           # bring your own requests.Session for retries/proxies
)
client.close()              # or use it as a context manager
```

Every call is logged at DEBUG on the `unipile` logger:
`logging.getLogger("unipile").setLevel(logging.DEBUG)`.

## Tests

```bash
python tests/test_sdk.py      # or: pytest
```
