From d86740d8612968c5b8267da5e38c175e0f9a0db7 Mon Sep 17 00:00:00 2001 From: Chris Piekarski Date: Fri, 22 Aug 2025 15:34:37 -0600 Subject: [PATCH] update readme and pytest path --- README.md | 148 +++++++++++++++++++++++++++++++++++++++++++++++++---- pytest.ini | 2 + 2 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 pytest.ini diff --git a/README.md b/README.md index 22ec62c..1012016 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,144 @@ +python-json-socket (jsocket) +============================ -The final Python 2 package version is 1.6.1 -Python 2 PyPi: https://pypi.org/project/jsocket/1.6.1/ -Python 3 PyPi: https://pypi.org/project/jsocket/1.7/ +[![CI](https://github.com/chris-piekarski/python-json-socket/actions/workflows/ci.yml/badge.svg)](https://github.com/chris-piekarski/python-json-socket/actions/workflows/ci.yml) +![PyPI](https://img.shields.io/pypi/v/jsocket.svg) +![Python Versions](https://img.shields.io/pypi/pyversions/jsocket.svg) +![License](https://img.shields.io/pypi/l/jsocket.svg) +Simple JSON-over-TCP sockets for Python. This library provides: -pip install jsocket +- JsonClient/JsonServer: length‑prefixed JSON message framing over TCP +- ThreadedServer: a single-connection server running in its own thread +- ServerFactory/ServerFactoryThread: a per‑connection worker model for multiple clients -Example: +It aims to be small, predictable, and easy to integrate in tests or small services. -See the examples_servers.py file for an example of how to use the threaded server factory feature of this module. -Can also upload the python_json_socket_3.ipynb notebook to Google colab -Unit Test: -Lettuce unit test exists under features directory -See that directories README for instructions on installing python3 lettuce fork +Install +------- + +``` +pip install jsocket +``` + +Requires Python 3.8+. + + +Quickstart +---------- + +Echo server with `ThreadedServer` and a client: + +```python +import time +import jsocket + +class Echo(jsocket.ThreadedServer): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.timeout = 2.0 + + # Return a dict to send a response back to the client + def _process_message(self, obj): + if isinstance(obj, dict) and 'echo' in obj: + return obj + return None + +# Bind to an ephemeral port (port=0) +server = Echo(address='127.0.0.1', port=0) +_, port = server.socket.getsockname() +server.start() + +client = jsocket.JsonClient(address='127.0.0.1', port=port) +assert client.connect() is True + +payload = {"echo": "hello"} +client.send_obj(payload) +assert client.read_obj() == payload + +client.close() +server.stop() +server.join() +``` + +Per‑connection workers with `ServerFactory`: + +```python +import jsocket + +class Worker(jsocket.ServerFactoryThread): + def __init__(self): + super().__init__() + self.timeout = 2.0 + + def _process_message(self, obj): + if isinstance(obj, dict) and 'message' in obj: + return {"reply": f"got: {obj['message']}"} + +server = jsocket.ServerFactory(Worker, address='127.0.0.1', port=5489) +server.start() +# Connect one or more clients; one Worker is spawned per connection +``` + + +API Highlights +-------------- + +- JsonClient: + - `connect()` returns True on success + - `send_obj(dict)` sends a JSON object + - `read_obj()` blocks until a full message is received; raises `socket.timeout` or `RuntimeError("socket connection broken")` + - `timeout` property controls socket timeouts + +- ThreadedServer: + - Subclass and implement `_process_message(self, obj) -> Optional[dict]` + - Return a dict to send a response; return `None` to send nothing + - `start()`, `stop()`, `join()` manage the server thread + - `send_obj(dict)` sends to the currently connected client + +- ServerFactory / ServerFactoryThread: + - `ServerFactoryThread` is a worker that handles one client connection + - `ServerFactory` accepts connections and spawns a worker per client + + +Examples and Tests +------------------ + +- Examples: see `examples/example_servers.py` and `scripts/smoke_test.py` +- Pytest: end-to-end and listener tests under `tests/` + - Run: `pytest -q` + + +Behavior-Driven Tests (Behave) +------------------------------ + +- Steps live under `features/steps/` and environment hooks in `features/environment.py`. +- To run Behave scenarios, add one or more `.feature` files under `features/` and run: + - `pip install -r requirements-dev.txt` + - `PYTHONPATH=. behave -f progress2` +- A minimal example feature: + + ```gherkin + Feature: Echo round-trip + Scenario: client/server echo + Given I start the server + And I connect the client + When the client sends the object {"echo": "hi"} + Then the client sees a message {"echo": "hi"} + ``` + + +Notes +----- + +- Message framing uses a 4‑byte big‑endian length header followed by a JSON payload encoded as UTF‑8. +- On disconnect, reads raise `RuntimeError("socket connection broken")` so callers can distinguish cleanly from timeouts. +- Binding with `port=0` lets the OS choose an ephemeral port; find it with `server.socket.getsockname()`. + + +Links +----- + +- PyPI: https://pypi.org/project/jsocket/ +- License: see `LICENSE` diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..a635c5c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = .