Metadata-Version: 2.5
Name: pyPhoneApi
Version: 0.1.0
Summary: Async Python library for phone number lookup, validation and formatting, built on aiohttp and phonenumbers.
Project-URL: Homepage, https://github.com/msctop4/pyPhoneApi/
Project-URL: Repository, https://github.com/msctop4/pyPhoneApi/
Author: pyPhoneApi contributors
License: MIT
Keywords: aiohttp,async,asyncio,lookup,phone,phonenumbers,validation
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: aiohttp>=3.9
Requires-Dist: phonenumbers>=8.13
Provides-Extra: test
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
Requires-Dist: pytest>=8.0; extra == 'test'
Description-Content-Type: text/markdown

# pyPhoneApi

Async Python library for phone number lookup, validation and formatting.

Built on top of `asyncio`, `aiohttp` and `phonenumbers`. No manually maintained
country/calling-code tables, no blocking calls on the event loop, no synchronous
public API.

## Installation

```bash
pip install pyPhoneApi
```

## Requirements

- Python >= 3.11
- aiohttp
- phonenumbers

## Features

- Fully async public API (`await phone.lookup(...)`)
- Reuses a single `aiohttp.ClientSession` across calls, never creates one per request
- Country detection, ISO 3166-1 alpha-2 code, calling code, national and
  international formatting powered by `phonenumbers`
- Validity and possibility checks (`is_valid`, `is_possible`)
- Number type detection (`MOBILE`, `FIXED_LINE`, `VOIP`, ...)
- Localized country names (`language="en"`, `language="ru"`, ...)
- Typed exceptions instead of generic errors
- Safe under concurrent load with `asyncio.gather`
- No emoji anywhere in output, logs, or exceptions
- Fully typed, ships with `py.typed`

## Quick example

```python
import asyncio

from pyPhoneApi import PhoneApi


async def main() -> None:
    async with PhoneApi() as phone:
        result = await phone.lookup("+380643732627")

        print(f"Country: {result.country}")
        print(f"Country code: {result.country_code}")
        print(f"Calling code: {result.calling_code}")
        print(f"Number: {result.number}")
        print(f"National: {result.national}")
        print(f"Valid: {result.is_valid}")
        print(f"Possible: {result.is_possible}")


asyncio.run(main())
```

Output:

```text
Country: Ukraine
Country code: UA
Calling code: +380
Number: +380643732627
National: 0643732627
Valid: True
Possible: True
```

## Using `async with`

```python
from pyPhoneApi import PhoneApi

async with PhoneApi() as phone:
    result = await phone.lookup("+380643732627")
```

## Manual session lifecycle

```python
from pyPhoneApi import PhoneApi

phone = PhoneApi()

await phone.start()

result = await phone.lookup("+380643732627")

await phone.close()
```

Repeated calls to `lookup()` reuse the same `aiohttp.ClientSession`. Calling
`lookup()` or `start()` after `close()` raises `ClientClosedError`.

## Shortcut function

```python
from pyPhoneApi import lookup

result = await lookup("+380643732627")
```

## Error handling

```python
from pyPhoneApi import PhoneApi
from pyPhoneApi import InvalidPhoneNumberError

async with PhoneApi() as phone:
    try:
        result = await phone.lookup("invalid-number")
    except InvalidPhoneNumberError as e:
        print(e)
```

Output:

```text
Invalid phone number
```

## Concurrent requests

```python
import asyncio

from pyPhoneApi import PhoneApi


async def main() -> None:
    async with PhoneApi() as phone:
        results = await asyncio.gather(
            phone.lookup("+380643732627"),
            phone.lookup("+37255555555"),
            phone.lookup("+491234567890"),
        )

        for result in results:
            print(result.country, result.number)


asyncio.run(main())
```

Only one `aiohttp.ClientSession` is created and shared across all of these
lookups, and the event loop is never blocked.

## Localized country names

```python
result = await phone.lookup("+380643732627", language="ru")
print(result.country)
```

```text
Украина
```

```python
result = await phone.lookup("+380643732627", language="en")
print(result.country)
```

```text
Ukraine
```

## `PhoneResult`

```python
@dataclass(frozen=True, slots=True)
class PhoneResult:
    number: str
    international: str
    national: str
    country: str
    country_code: str
    calling_code: str
    is_valid: bool
    is_possible: bool
    number_type: str
```

| Field           | Description                                              |
|-----------------|------------------------------------------------------------|
| `number`        | Number in E.164 format                                    |
| `international` | Number in international format                            |
| `national`      | Number in national format, without separators              |
| `country`       | Localized country name                                    |
| `country_code`  | ISO 3166-1 alpha-2 country code (e.g. `UA`)                |
| `calling_code`  | Calling code with leading `+` (e.g. `+380`)                |
| `is_valid`      | Whether the number is a valid number for its region        |
| `is_possible`   | Whether the number is a possible number for its region     |
| `number_type`   | `MOBILE`, `FIXED_LINE`, `FIXED_LINE_OR_MOBILE`, `TOLL_FREE`, `PREMIUM_RATE`, `VOIP`, or `UNKNOWN` |

`to_dict()`:

```python
{
    "number": "+380643732627",
    "international": "+380643732627",
    "national": "0643732627",
    "country": "Ukraine",
    "country_code": "UA",
    "calling_code": "+380",
    "is_valid": True,
    "is_possible": True,
    "number_type": "MOBILE"
}
```

## Exceptions

| Exception                   | Raised when                                              |
|------------------------------|-----------------------------------------------------------|
| `PhoneApiError`              | Base exception for all library errors                     |
| `InvalidPhoneNumberError`    | The number is not a valid or possible phone number         |
| `NumberParseError`           | The number could not be parsed at all                     |
| `UnsupportedCountryError`    | No region could be determined for the number               |
| `ClientClosedError`          | `lookup()` or `start()` is called after `close()`          |

## API reference

### `class PhoneApi(*, language: str = "en")`

- `await phone.start() -> None` — creates the underlying `aiohttp.ClientSession` if one does not already exist.
- `await phone.close() -> None` — closes the session and marks the client as closed.
- `await phone.lookup(number: str, *, language: str | None = None) -> PhoneResult` — parses and analyzes a phone number.
- `async with PhoneApi() as phone:` — starts the session on enter, closes it on exit.
- `phone.is_closed -> bool` — whether the client has been closed.

### `async def lookup(number: str, *, language: str = "en") -> PhoneResult`

Shortcut that creates a `PhoneApi`, performs a single lookup, and closes the
session automatically.

## Running tests

```bash
pip install -e ".[test]"
pytest
```

## License

MIT
