Skip to content

Commit

Permalink
Initial documentation (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
francis-clairicia authored Sep 21, 2023
1 parent d5ed719 commit f31375c
Show file tree
Hide file tree
Showing 232 changed files with 12,141 additions and 2,304 deletions.
3 changes: 3 additions & 0 deletions .bandit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ exclude_dirs: ['tests']
skips: [
'B403', # Imports of "pickle"
]

assert_used:
skips: ['docs/**/*.py']
2 changes: 2 additions & 0 deletions .bumpversion.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ values =
first_value = 1

[bumpversion:file:src/easynetwork/__init__.py]

[bumpversion:file:docs/source/conf.py]
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ per-file-ignores =
# F401: 'module' imported but unused
# F403: 'from module import *' used; unable to detect undefined names
# F405: 'name' may be undefined, or defined from star imports: 'module'
# F841: local variable 'name' is assigned to but never used
# DALL001: There is no __all__ defined
src/easynetwork/__init__.py:F401,F403,F405
src/easynetwork/*/__init__.py:F401,F403,F405
tests/*.py:DALL001
docs/*.py:DALL001
docs/source/_include/*.py:F841,DALL001
2 changes: 1 addition & 1 deletion .github/actions/setup-tox/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ runs:
shell: bash
- uses: pdm-project/setup-pdm@v3
with:
version: '2.8.2'
version: '2.9.2'
python-version: ${{ inputs.python-version }}
cache: true
cache-dependency-path: './pdm.lock'
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:
- '.github/**'
- '!.github/actions/setup-tox/**'
- '!.github/workflows/build.yml'
- 'docs/**'
push:
branches:
- main
Expand All @@ -32,6 +33,7 @@ on:
- '.github/**'
- '!.github/actions/setup-tox/**'
- '!.github/workflows/build.yml'
- 'docs/**'
workflow_dispatch:
inputs:
SOURCE_DATE_EPOCH:
Expand Down
23 changes: 20 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ ci:

repos:
- repo: https://github.com/asottile/pyupgrade
rev: v3.10.1
rev: v3.11.0
hooks:
- id: pyupgrade
args: ['--py311-plus']
Expand All @@ -31,13 +31,22 @@ repos:
types_or: [python, pyi]
require_serial: true
pass_filenames: false
- id: mypy
name: mypy (docs)
files: ^((src|docs/source)/)
exclude: ^(docs/source/conf.py)$
entry: tox run -q -e mypy-docs
language: system
types_or: [python, pyi]
require_serial: true
pass_filenames: false
- repo: https://github.com/PyCQA/isort
rev: '5.12.0'
hooks:
- id: isort
args: ['--filter-files', '--settings-file', 'pyproject.toml']
- repo: https://github.com/psf/black
rev: '23.7.0'
rev: '23.9.1'
hooks:
- id: black
args: ['--config', 'pyproject.toml']
Expand All @@ -64,9 +73,17 @@ repos:
types: [] # Overwrite with empty in order to fallback to types_or
types_or: [python, pyi]
- repo: https://github.com/pdm-project/pdm
rev: '2.8.2'
rev: '2.9.2'
hooks:
- id: pdm-lock-check
- repo: https://github.com/pre-commit/pygrep-hooks
rev: 'v1.10.0'
hooks:
- id: python-check-blanket-noqa
- id: python-no-log-warn
- id: rst-backticks
- id: rst-directive-colons
- id: rst-inline-touching-normal
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: 'v4.4.0'
hooks:
Expand Down
6 changes: 6 additions & 0 deletions .vscode/settings.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
130
]
},
"[restructuredtext]": {
"editor.tabSize": 3,
"editor.rulers": [
150
]
},
"black-formatter.args": [
"--config",
"pyproject.toml",
Expand Down
175 changes: 174 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,175 @@
# EasyNetwork
The easiest way to use sockets in Python

The easiest way to use sockets in Python!

[![PyPI](https://img.shields.io/pypi/v/easynetwork)](https://pypi.org/project/easynetwork/)
[![PyPI - License](https://img.shields.io/pypi/l/easynetwork)](https://github.com/francis-clairicia/EasyNetwork/blob/main/LICENSE)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/easynetwork)

[![Test](https://github.com/francis-clairicia/EasyNetwork/actions/workflows/test.yml/badge.svg)](https://github.com/francis-clairicia/EasyNetwork/actions/workflows/test.yml)
[![Codecov](https://img.shields.io/codecov/c/github/francis-clairicia/EasyNetwork)](https://codecov.io/gh/francis-clairicia/EasyNetwork)
[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/francis-clairicia/EasyNetwork)](https://www.codefactor.io/repository/github/francis-clairicia/easynetwork)

[![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/francis-clairicia/EasyNetwork/main.svg)](https://results.pre-commit.ci/latest/github/francis-clairicia/EasyNetwork/main)

[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
[![security: bandit](https://img.shields.io/badge/security-bandit-yellow.svg)](https://github.com/PyCQA/bandit)

[![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch)
[![pdm-managed](https://img.shields.io/badge/pdm-managed-blueviolet)](https://pdm.fming.dev)

## Installation
### From PyPI repository
```sh
pip install --user easynetwork
```

### From source
```sh
git clone https://github.com/francis-clairicia/EasyNetwork.git
cd EasyNetwork
pip install --user .
```

## Overview
EasyNetwork completely encapsulates the socket handling, providing you with a higher level interface
that allows an application/software to completely handle the logic part with Python objects,
without worrying about how to process, send or receive data over the network.

The communication protocol can be whatever you want, be it JSON, Pickle, ASCII, structure, base64 encoded,
compressed, encrypted, or any other format that is not part of the standard library.
You choose the data format and the library takes care of the rest.

Works with TCP and UDP.

### Documentation
Coming soon.

## Usage
### TCP Echo server with JSON data
```py
import logging
from collections.abc import AsyncGenerator
from typing import Any, TypeAlias

from easynetwork.api_async.server import AsyncStreamClient, AsyncStreamRequestHandler
from easynetwork.api_sync.server import StandaloneTCPNetworkServer
from easynetwork.exceptions import StreamProtocolParseError
from easynetwork.protocol import StreamProtocol
from easynetwork.serializers import JSONSerializer

# These TypeAliases are there to help you understand
# where requests and responses are used in the code
RequestType: TypeAlias = Any
ResponseType: TypeAlias = Any


class JSONProtocol(StreamProtocol[ResponseType, RequestType]):
def __init__(self) -> None:
super().__init__(JSONSerializer())


class EchoRequestHandler(AsyncStreamRequestHandler[RequestType, ResponseType]):
def __init__(self) -> None:
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)

async def handle(
self,
client: AsyncStreamClient[ResponseType],
) -> AsyncGenerator[None, RequestType]:
try:
request: RequestType = yield # A JSON request has been sent by this client
except StreamProtocolParseError:
# Invalid JSON data sent
# This is an example of how you can answer to an invalid request
await client.send_packet({"error": "Invalid JSON", "code": "parse_error"})
return

self.logger.info(f"{client.address} sent {request!r}")

# As a good echo handler, the request is sent back to the client
response: ResponseType = request
await client.send_packet(response)

# Leaving the generator will NOT close the connection,
# a new generator will be created afterwards.
# You may manually close the connection if you want to:
# await client.aclose()


def main() -> None:
host = None # Bind on all interfaces
port = 9000

logging.basicConfig(level=logging.INFO, format="[ %(levelname)s ] [ %(name)s ] %(message)s")
with StandaloneTCPNetworkServer(host, port, JSONProtocol(), EchoRequestHandler()) as server:
try:
server.serve_forever()
except KeyboardInterrupt:
pass


if __name__ == "__main__":
main()
```

### TCP Echo client with JSON data
```py
from typing import Any

from easynetwork.api_sync.client import TCPNetworkClient
from easynetwork.protocol import StreamProtocol
from easynetwork.serializers import JSONSerializer


class JSONProtocol(StreamProtocol[Any, Any]):
def __init__(self) -> None:
super().__init__(JSONSerializer())


def main() -> None:
with TCPNetworkClient(("localhost", 9000), JSONProtocol()) as client:
client.send_packet({"data": {"my_body": ["as json"]}})
response = client.recv_packet() # response should be the sent dictionary
print(response) # prints {'data': {'my_body': ['as json']}}


if __name__ == "__main__":
main()
```

<details markdown="1">
<summary>Asynchronous version ( with <code>async def</code> )</summary>

```py
import asyncio
from typing import Any

from easynetwork.api_async.client import AsyncTCPNetworkClient
from easynetwork.protocol import StreamProtocol
from easynetwork.serializers import JSONSerializer


class JSONProtocol(StreamProtocol[Any, Any]):
def __init__(self) -> None:
super().__init__(JSONSerializer())


async def main() -> None:
async with AsyncTCPNetworkClient(("localhost", 9000), JSONProtocol()) as client:
await client.send_packet({"data": {"my_body": ["as json"]}})
response = await client.recv_packet() # response should be the sent dictionary
print(response) # prints {'data': {'my_body': ['as json']}}


if __name__ == "__main__":
asyncio.run(main())
```

</details>

## License
This project is licensed under the terms of the [Apache Software License 2.0](https://github.com/francis-clairicia/EasyNetwork/blob/main/LICENSE).
2 changes: 2 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Intersphinx inventory dump.
*-inv.txt
20 changes: 20 additions & 0 deletions docs/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build

# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
35 changes: 35 additions & 0 deletions docs/make.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
@ECHO OFF

pushd %~dp0

REM Command file for Sphinx documentation

if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build

%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)

if "%1" == "" goto help

%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end

:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%

:end
popd
31 changes: 31 additions & 0 deletions docs/source/_extensions/sphinx_easynetwork.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from sphinx.application import Sphinx

from easynetwork.api_sync.server import AbstractNetworkServer
from easynetwork.api_sync.server._base import BaseStandaloneNetworkServerImpl


def _replace_base_in_place(klass: type, bases: list[type], base_to_replace: type, base_to_set_instead: type) -> None:
if issubclass(klass, base_to_replace):
for index, base in enumerate(bases):
if base is base_to_replace:
bases[index] = base_to_set_instead


def autodoc_process_bases(app: Sphinx, name: str, obj: type, options: dict[str, Any], bases: list[type]) -> None:
_replace_base_in_place(obj, bases, BaseStandaloneNetworkServerImpl, AbstractNetworkServer)


def setup(app: Sphinx) -> dict[str, Any]:
app.setup_extension("sphinx.ext.autodoc")
app.connect("autodoc-process-bases", autodoc_process_bases)

return {
"version": "0.1",
"parallel_read_safe": True,
"parallel_write_safe": True,
}
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from __future__ import annotations

from typing import Any, TypeAlias

from easynetwork.protocol import DatagramProtocol
from easynetwork.serializers import JSONSerializer

SentPacket: TypeAlias = Any
ReceivedPacket: TypeAlias = Any

json_protocol: DatagramProtocol[SentPacket, ReceivedPacket] = DatagramProtocol(JSONSerializer())
Loading

0 comments on commit f31375c

Please sign in to comment.