diff --git a/DEVELOPER.md b/DEVELOPER.md index 8168a3e3..8e63b6d4 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -1,47 +1,71 @@ -# Developers manual +# Developer guide ## Supported Python versions -We support Python 3.7, 3.8, 3.9, 3.10 -## Supported platforms -We maintain and test our SDK using Travis.CI and Ubuntu. -Windows/MacOS/BSD platforms support was verified only once, after SDK v4.0 release. We did not test the newer releases with these platforms. +Python 3.9–3.13. This matches `python_requires` in `setup.py` and the CI matrix in `.github/workflows/run-tests.yml`. + +## Setup + +Use a virtual environment, then install the development dependencies from the repository root: + +```sh +./scripts/install.sh +``` + +## Tests + +Run the unit, functional, and integrational tests (with coverage): + +```sh +python scripts/run-tests.py +``` + +This runs `py.test tests --cov=pubnub --ignore=tests/manual/`. Integrational tests replay recorded HTTP interactions (VCR cassettes), so they don't need real keys. + +Acceptance tests use `behave`. The `.feature` files come from the `sdk-specifications` repository and are copied into `tests/acceptance/` by CI (see the `acceptance` job in `run-tests.yml`). They run against the PubNub mock server, so they are not practical to run locally without it. Run one suite with, for example, `behave tests/acceptance/pam`. + +## Linting + +CI runs flake8 in `.github/workflows/run-validations.yml`. To run the same check locally: + +```sh +flake8 --exclude=scripts/,src/,.cache,.git,.idea,.tox,._trial_temp/,venv/ --ignore F811,E402 +``` + +## Pull requests + +- Include tests for behavioral changes and keep the test matrix green on all supported Python versions. + +## Clients and call styles + +`PubNub` (native threads) and `PubNubAsyncio` (`pubnub.pubnub_asyncio`) share the same endpoint builders. The method that finishes the call decides how it runs. -## Event Loop Frameworks ### Native (`threading`) -Native implementation concerns using `requests` library (https://github.com/requests/requests), a wrapper for a lower level urllib3 (https://github.com/shazow/urllib3). -urllib2 is not supported, there is an outline of request handler for it (which doesn't work, just the outline) can be found at (https://github.com/pubnub/python/blob/master/pubnub/request_handlers/urllib2_handler.py). -All listed Python versions are supported. -#### sync -Synchronous calls can be invoked by using `sync()` call. This will return Envelope object https://github.com/pubnub/python/blob/037a6829c341471c2c78a7a429f02dec671fd791/pubnub/structures.py#L79-L82 which wraps both Result and Status. All exceptions are triggered natively using `raise Exception` syntax. The idea was to use 2 types of final execution methods like in Asyncio/Tornado. These fixes are postponed until next major release (v5.0.0): -- `result()` should return just Response and natively raise an exception if there is one -- `sync()` should return Envelope(as is now), but do not raise any exceptions -The work on it has been started in branch 'fix-errors-handling', but as were mentioned above, was postponed. +`PubNub` uses `HttpxRequestHandler` (with HTTP/2 support) by default. A different handler can be passed as `custom_request_handler`, set with `set_request_handler`, or selected with the `PUBNUB_REQUEST_HANDLER` environment variable. A `requests`-based handler is also included. -#### async -Asynchronous calls are implemented by using threads (`threading` module https://docs.python.org/3/library/threading.html). The passed-in to async() functinon callback will be called with a response or an error. +- `sync()` blocks and returns an `Envelope` (`result` and `status`). If the request fails, it raises the underlying exception. +- `pn_async(callback)` runs the request on a thread and calls `callback(result, status)`. Check `status.is_error()` for failures. ### Asyncio -Asyncio library is supported since Python 3.4. -There are 2 types of calls: -- using `result()` - only a result will be returned; in case of exception it will be raised natively -- using `future()` - a wrapper (Envelope) for a result and a status; in case of exception it can be checked using env.is_error() -You can find more examples here https://github.com/pubnub/python/blob/master/tests/integrational/asyncio/test_invocations.py +Create the client with `PubNubAsyncio` and call these inside the event loop: -## Tests -* Test runner: py.test -* Source code checker: flake -* BDD tests runner: behave - one needs to place needed feature file under acceptance/name_of_the_feature directory. - An example: `behave tests/acceptance/pam` +- `await ....result()` returns the result and raises on failure. +- `await ....future()` returns an `Envelope`. Check `envelope.status.is_error()` for failures. + +For more usage, see `tests/integrational/asyncio/test_invocations.py`. -## Daemon mode with Native SDK -Daemon mode for all requests are disabled by default. This means that all asynchronous requests including will block the main thread until all the children be closed. If SDK user want to use Java-like behaviour when it's up to him to decide should he wait for response completion or continue program execution, he has to explicitly set daemon mode to true: +## Daemon mode (native threads) + +`config.daemon` defaults to `False`. Threads started by `pn_async()` are then non-daemon, so the interpreter waits for them before exiting. To let the process exit without waiting for in-flight asynchronous requests, set it before making calls: ```python -pubnub.config.daemon = True +pnconfig.daemon = True ``` -## SubscribeListener -SubscribeListeners for all implementations are helpers developed to simplify tests behaviour. They can be used by SDK end-user, but are not well tested and can't be found in any documentation. +This setting is read when each `pn_async()` request thread is created. It does not affect the subscribe loop or the heartbeat timer, which always run as daemon threads. + +## SubscribeListener helpers + +`pubnub.pubnub.SubscribeListener` (native) and `pubnub.pubnub_asyncio.SubscribeListener` (asyncio) are helpers that expose connection state and queue incoming events so code (mostly tests) can wait for them. They are documented in their docstrings and used by `examples/native_threads/subscribe.py` and the integrational tests. They are not covered by the public SDK documentation, so prefer `SubscribeCallback` in application code. diff --git a/README.md b/README.md index e713b1fd..b1fb8163 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,164 @@ # PubNub Python SDK -[![Build Status](https://app.travis-ci.com/pubnub/python.svg?branch=master)](https://app.travis-ci.com/pubnub/python) -[![PyPI](https://img.shields.io/pypi/v/pubnub.svg)](https://pypi.python.org/pypi/pubnub/) -[![PyPI](https://img.shields.io/pypi/pyversions/pubnub.svg)](https://pypi.python.org/pypi/pubnub/) -[![Docs](https://img.shields.io/badge/docs-online-blue.svg)](https://www.pubnub.com/docs/python/pubnub-python-sdk-v4) +[![PyPI](https://img.shields.io/pypi/v/pubnub.svg)](https://pypi.org/project/pubnub/) -This is the official PubNub Python SDK repository. +PubNub provides global infrastructure for real-time, interactive applications. -**Note:** Python SDK version 5.0 no longer supports Python 2.7, Twisted or Tornado, if you still require support for these please use SDK version 4.8.1 +Publish and receive messages in Python. Use this SDK for Python server, CLI, desktop, IoT, and Asyncio applications. -PubNub takes care of the infrastructure and APIs needed for the realtime communication layer of your application. Work on your app's logic and let PubNub handle sending and receiving data across the world in less than 100ms. +[Documentation](https://www.pubnub.com/docs/sdks/python) · [API reference](https://www.pubnub.com/docs/sdks/python/api-reference/publish-and-subscribe) · [Changelog](https://www.pubnub.com/docs/sdks/python/changelog) · [Support](https://support.pubnub.com/) -## Get keys +## Requirements -You will need the publish and subscribe keys to authenticate your app. Get your keys from the [Admin Portal](https://dashboard.pubnub.com/login). +| Requirement | Supported version or setup | +| --- | --- | +| Python | [Python 3.9 or later](https://www.pubnub.com/docs/sdks/python) | +| Supported Python versions | [Python 3.9–3.13 and supported operating systems](https://www.pubnub.com/docs/sdks/python/platform-support) | +| pip | Install the package into the same Python environment used to run the application. A virtual environment is recommended. | -## Configure PubNub +## Installation -1. Integrate the Python SDK into your project using `pip`: +From the project or active virtual environment: - ```bash - pip install pubnub - ``` +```sh +pip install pubnub +``` + +For other installation methods, see [environment setup](https://www.pubnub.com/docs/sdks/python). + +## Quickstart + +This example runs in Python 3.9 or later. It subscribes to `hello_world`, publishes one message, and prints the received text. + +### Get your keys + +1. Open the [PubNub Admin Portal](https://admin.pubnub.com/). +2. Create an app and a keyset for development, or select an existing development keyset. +3. Copy its publish key and subscribe key. -2. Configure your keys: +If Access Manager is enabled, obtain a token from your trusted backend and call `pubnub.set_token(token)`. Keep the secret key on the backend. - ```python - pnconfig = PNConfiguration() +### Send and receive a message - pnconfig.subscribe_key = 'mySubscribeKey' - pnconfig.publish_key = 'myPublishKey' - pnconfig.uuid = 'myUniqueUUID' - pubnub = PubNub(pnconfig) - ``` +Create `app.py` in the project directory and add: -## Add event listeners +Replace the key placeholders. Use a User ID that identifies the user or device in your app. ```python -class SubscribeHandler(SubscribeCallback): - def status(self, pubnub, event): - print("Is there an error? ", event.is_error()) - print("Status value for category: %s" % event.category) - print("Status value for error_data: %s" % event.error_data) - print("Status value for error: %s" % event.error) - print("Status value for status_code: %s" % event.status_code) - print("Status value for operation: %s" % event.operation) - print("Status value for tls_enabled: %s" % event.tls_enabled) - print("Status value for uuid: %s" % event.uuid) - print("Status value for auth_key: %s" % event.auth_key) - print("Status value for origin: %s" % event.origin) - print("Status value for client_request: %s" % event.client_request) - print("Status value for client_response: %s" % event.client_response) - print("Status value for original_response: %s" % event.original_response) - print("Status value for affected_channels: %s" % event.affected_channels) - print("Status value for affected_groups: %s" % event.affected_groups) - - def presence(self, pubnub, presence): - pass # Handle incoming presence data - - def message(self, pubnub, message): - pass # Handle incoming messages - - def signal(self, pubnub, signal): - pass # Handle incoming signals - -pubnub.add_listener(SubscribeHandler()) +from threading import Event + +from pubnub.callbacks import SubscribeCallback +from pubnub.enums import PNStatusCategory +from pubnub.pnconfiguration import PNConfiguration +from pubnub.pubnub import PubNub + + +class Listener(SubscribeCallback): + def __init__(self): + self.connected = Event() + self.received = Event() + + def status(self, pubnub, status): + if status.category == PNStatusCategory.PNConnectedCategory: + self.connected.set() + + def message(self, pubnub, message): + print(message.message) + self.received.set() + + def presence(self, pubnub, presence): + pass + + +config = PNConfiguration() +config.subscribe_key = "YOUR_SUBSCRIBE_KEY" +config.publish_key = "YOUR_PUBLISH_KEY" +config.user_id = "hello-world-user" + +pubnub = PubNub(config) +listener = Listener() +pubnub.add_listener(listener) + +try: + pubnub.subscribe().channels("hello_world").execute() + + if not listener.connected.wait(timeout=10): + raise TimeoutError("Timed out waiting for the subscription to connect") + + pubnub.publish().channel("hello_world").message("Hello world").sync() + + if not listener.received.wait(timeout=10): + raise TimeoutError("Timed out waiting for the message") +finally: + pubnub.unsubscribe().channels("hello_world").execute() + pubnub.remove_listener(listener) + pubnub.stop() ``` -## Publish/subscribe +### Run the example -```python -def my_publish_callback(envelope, status): - if status.is_error(): - ... #handle error here - else: - ... #handle result here +```sh +python app.py +``` -pubnub.publish().channel('my_channel').message('Hello world!').pn_async(my_publish_callback) +The terminal should show: -pubnub.subscribe().channels('my_channel').execute() ``` +Hello world +``` + +The example unsubscribes from `hello_world`, removes its listener with `pubnub.remove_listener(listener)`, and calls `pubnub.stop()` before exit. + +For a complete application, see the [getting started guide](https://www.pubnub.com/docs/sdks/python). + +## Next steps + +| Task | Documentation | +| --- | --- | +| Configure the client | [Configuration](https://www.pubnub.com/docs/sdks/python/api-reference/configuration) | +| Work with subscriptions and messages | [Publish & Subscribe](https://www.pubnub.com/docs/sdks/python/api-reference/publish-and-subscribe) | +| Check channel occupancy | [Presence](https://www.pubnub.com/docs/sdks/python/api-reference/presence) | +| Read message history | [Message Persistence](https://www.pubnub.com/docs/sdks/python/api-reference/storage-and-playback) | + +## Build with an AI coding assistant + +The [PubNub MCP server](https://www.pubnub.com/docs/ai/pubnub-mcp-server) gives an AI coding assistant access to PubNub SDK documentation and PubNub APIs. Connect the assistant to the hosted server at `https://mcp.pubnub.com`, or run `npx @pubnub/mcp@latest` locally. + +The [server repository](https://github.com/pubnub/pubnub-mcp-server) has setup steps for VS Code, Cursor, Claude Code, Claude Desktop, Codex, Gemini CLI, and OpenCode. + +## Before production + +Use [Access Manager](https://www.pubnub.com/docs/sdks/python/api-reference/access-manager) to grant each client the permissions it needs. Keep the secret key on your backend. Never include it in a distributed client. + +Reuse the PubNub client for the application/service lifetime. Unsubscribe streams that are no longer required, remove listeners with `pubnub.remove_listener(listener)`, and call `pubnub.stop()` when the client is shutting down. + +Live delivery through PubNub SDKs is at-most-once. A subscriber can miss messages while disconnected or if its buffer overflows. For longer-gap recovery, see [Message Persistence](https://www.pubnub.com/docs/sdks/python/api-reference/storage-and-playback). + +## Troubleshooting + +| Symptom | Check | +| --- | --- | +| Python reports `ModuleNotFoundError: No module named 'pubnub'` | Install PubNub into the same Python environment that runs the application and verify Python 3.9+. See the [Python SDK guide](https://www.pubnub.com/docs/sdks/python). | +| A publish succeeds but no message appears | Check the message handler, subscription readiness, keyset, and channel name. | +| The Python process remains active after the application no longer needs PubNub | Unsubscribe active streams, remove listeners, and call `pubnub.stop()` during shutdown. See [Python troubleshooting](https://www.pubnub.com/docs/sdks/python/troubleshoot). | +| Extra clients or duplicate messages during development | For web frameworks and development reloaders, create the PubNub client at application/service scope instead of on every request. Stop the owned client as part of application shutdown. | + +For setup help, see [troubleshooting](https://www.pubnub.com/docs/sdks/python/troubleshoot). Check [network status](https://status.pubnub.com/) for service incidents. + +## Releases + +Read the [changelog](https://www.pubnub.com/docs/sdks/python/changelog) before upgrading. + +For a major version change, review the [Python SDK changelog](https://www.pubnub.com/docs/sdks/python/changelog) for breaking changes before upgrading. + +## Support and contributions + +For setup or account help, contact [PubNub Support](https://support.pubnub.com/). -## Documentation +For a reproducible SDK bug, use [GitHub Issues](https://github.com/pubnub/python/issues). Include the SDK version, runtime, and a small reproduction with credentials removed. -* [Build your first realtime Python app with PubNub](https://www.pubnub.com/docs/general/basics/set-up-your-account) -* [API reference for Python](https://www.pubnub.com/docs/sdks/python) +Install the repository development dependencies, run the supported Python test matrix, and include tests for behavioral changes before opening a pull request. -## Support +## License -If you **need help** or have a **general question**, contact support@pubnub.com. +See the [PubNub Software Development Kit License](https://github.com/pubnub/python/blob/master/LICENSE).