Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
KOLANICH committed Oct 24, 2023
0 parents commit 44f3015
Show file tree
Hide file tree
Showing 19 changed files with 633 additions and 0 deletions.
Empty file added .ci/aptPackagesToInstall.txt
Empty file.
Empty file.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
indent_style = tab
indent_size = 4
insert_final_newline = true
end_of_line = lf

[*.{yml,yaml}]
indent_style = space
indent_size = 2
1 change: 1 addition & 0 deletions .github/.templateMarker
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
KOLANICH/python_project_boilerplate.py
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
allow:
- dependency-type: "all"
15 changes: 15 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: typical python workflow
uses: KOLANICH-GHActions/typical-python-workflow@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
__pycache__
*.py[co]
/*.egg-info
*.srctrlbm
*.srctrldb
build
dist
.eggs
monkeytype.sqlite3
/.ipynb_checkpoints
51 changes: 51 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
image: registry.gitlab.com/kolanich-subgroups/docker-images/fixed_python:latest

variables:
DOCKER_DRIVER: overlay2
SAST_ANALYZER_IMAGE_TAG: latest
SAST_DISABLE_DIND: "true"
SAST_CONFIDENCE_LEVEL: 5
CODECLIMATE_VERSION: latest

include:
- template: SAST.gitlab-ci.yml
- template: Code-Quality.gitlab-ci.yml
- template: License-Management.gitlab-ci.yml

build:
tags:
- shared
- linux
stage: build
variables:
GIT_DEPTH: "1"
PYTHONUSERBASE: ${CI_PROJECT_DIR}/python_user_packages

before_script:
- export PATH="$PATH:$PYTHONUSERBASE/bin" # don't move into `variables`
- apt-get update
# todo:
#- apt-get -y install
#- pip3 install --upgrade
#- python3 ./fix_python_modules_paths.py

script:
- python3 -m build -nw bdist_wheel
- mv ./dist/*.whl ./dist/SaneIO-0.CI-py3-none-any.whl
- pip3 install --upgrade ./dist/*.whl
- coverage run --source=SaneIO -m --branch pytest --junitxml=./rspec.xml ./tests/test.py
- coverage report -m
- coverage xml

coverage: "/^TOTAL(?:\\s+\\d+){4}\\s+(\\d+%).+/"

cache:
paths:
- $PYTHONUSERBASE

artifacts:
paths:
- dist
reports:
junit: ./rspec.xml
cobertura: ./coverage.xml
1 change: 1 addition & 0 deletions Code_Of_Conduct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
No codes of conduct!
4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include UNLICENSE
include *.md
include tests
include .editorconfig
24 changes: 24 additions & 0 deletions ReadMe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
SaneIO.py [![Unlicensed work](https://raw.githubusercontent.com/unlicense/unlicense.org/master/static/favicon.png)](https://unlicense.org/)
=========
~~[wheel (GHA via `nightly.link`)](https://nightly.link/KOLANICH-libs/SaneIO.py/workflows/CI/master/SaneIO-0.CI-py3-none-any.whl)~~
~~[![GitHub Actions](https://github.com/KOLANICH-libs/SaneIO.py/workflows/CI/badge.svg)](https://github.com/KOLANICH-libs/SaneIO.py/actions/)~~
![N∅ hard dependencies](https://shields.io/badge/-N∅_Ъ_deps!-0F0)
[![Libraries.io Status](https://img.shields.io/librariesio/github/KOLANICH-libs/SaneIO.py.svg)](https://libraries.io/github/KOLANICH-libs/SaneIO.py)
[![Code style: antiflash](https://img.shields.io/badge/code%20style-antiflash-FFF.svg)](https://codeberg.org/KOLANICH-tools/antiflash.py)

A very simplified and limited IO framework.

1. It is simple to use. It comes at cost.
1. Count of callback funtions is minimized.
2. Every object MUST inherit certain classes.
3. Batteries included.

2. It is "portable" in the sense the ports of this framework to other languages and platforms should keep the same structure, allowing the programs using it be ported more easily.
1. The core is decoupled from concrete implementations.
2. Upper level protocols are "Sans-IO". They don't depend on concrete IO implementations. Instead they depend on the interfaces provided by the framework.
3. Composition over inheritance -> can be ported C and Rust support

3. It is composable. The structure is a stack of objects.
* Wanna change the protocol in the stack? Just replace the layer object!
* Wanna access the same server using both TCP and UART? Just add a mux!
* Wanna access multiple upper layer protocols over the same low-level protocol (i.e. TCP)? Again, just add a mux!
1 change: 1 addition & 0 deletions SaneIO/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

261 changes: 261 additions & 0 deletions SaneIO/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import typing
from collections.abc import MutableMapping, MutableSequence
from queue import Queue
from warnings import warn

"""Python AsyncIO is complex shit. Let's wrap it a bit into a simple impl, detached from AsyncIO and IO in general, compatible to SansI/O fashion.
Design goals: be mimimalistic, simple and portable to different languages and architectures.
So composition over inheritance style is preferred..
"""


class SIOLayer:
__slots__ = ("higher", "lower")

def __init__(self):
self.higher = None # type: typing.Union["SIOResponder", "SIOSender"]
self.lower = None # type: "SIOTransport"

def bindHigher(self, higher):
self.higher = higher
higher._bindLower(self)

def unbindHigher(self, higher):
self.higher._unbindLower(self)
self.higher = None

def _bindLower(self, lower: "SIOTransport"):
self.lower = lower

def _unbindLower(self, lower: "SIOTransport"):
self.lower = None

@property
def lowestLevelResourceID(self) -> typing.Optional[typing.Any]:
"""The unique lowest level stuff that the layer uniquiely takes. It can be a TCP or UDP port, or file descriptor. It is intended to be used as a key for muxers.
`None` means the layer cannot be multiplexed. Usually it is because I have not invented a good way how to multiplex the lower level resources for now."""
return self.lower.lowestLevelResourceID


class SIOResponder(SIOLayer):
"""The entity that can receive data and return responses to them. Previous level is ALWAYS provided as a pointer. It cannot initiate connections itself."""

__slots__ = ()

def onReceive(self, lowerLevelTransport: "SIOTransport", data: bytes):
raise NotImplementedError(self.__class__, "onReceive")


class SIOSender(SIOLayer):
__slots__ = ()

def sendBytes(self, data: bytes):
raise NotImplementedError(self.__class__, "sendBytes")


class SIOTransport(SIOResponder, SIOSender):
"""The entity that can unwrap the lower level of protocol into the higher level.
Important: Servers also belong here.
By default it just passes through to the lower and upper layers"""

__slots__ = ()

def onReceive(self, proto, commandRaw: bytes):
return self.higher.onReceive(proto, commandRaw)

def sendBytes(self, data: bytes):
return self.lower.sendBytes(data)


class SIO1HiNLoMux(SIOTransport, MutableMapping):
"""A multiplexor from higher level to lower ones.
One higher level corresponds to multiple lower levels. All the messages sent from higher levels are copied to all the lower levels. Any message from any of lower levels is passed indiscriminately to higher levels."""

__slots__ = ()

def __init__(self):
super().__init__()
self.lower = {}

def __getitem__(self, k):
return self.lower[k]

def __setitem__(self, k, v):
self.lower[k] = v

def __delitem__(self, k):
del self.lower[k]

def __iter__(self):
return self.lower.keys()

def __len__(self):
return len(self.lower)

def _bindLower(self, lower: "SIOTransport"):
self[lower.lowestLevelResourceID] = lower

def _unbindLower(self, lower: "SIOTransport"):
del self[lower.lowestLevelResourceID]

def lowestLevelResourceID(self):
return None

def onReceive(self, lowerLevelTransport: "SIOTransport", data: bytes):
return self.higher.onReceive(self, data)

def sendBytes(self, data: bytes):
for el in self.lower.values():
el.sendBytes(data)


class HigherMultiplexableSIOResponder(SIOResponder):
"""A mixin class that defines the facilities needed to map raw bytes to upper levels"""

__slots__ = ()

def ifThisResponder(self, data: bytes):
raise NotImplementedError


class SIONHi1LoMux(SIOTransport, MutableMapping):
"""A multiplexor from lower level to higher ones.
Multiple higher levels correspond to single lower level. All the messages received get through the upper level target identification. Then when the level is identified, the message is sent to it."""

__slots__ = ()

def __init__(self):
super().__init__()
self.higher = []

def __getitem__(self, k):
return self.higher[k]

def __setitem__(self, k, v):
self.higher[k] = v

def __delitem__(self, k):
del self.higher[k]

def __iter__(self):
return self.higher

def __len__(self):
return len(self.higher)

def bindHigher(self, higher: HigherMultiplexableSIOResponder):
self.higher.append(higher)

def unbindHigher(self, higher: HigherMultiplexableSIOResponder):
del self[a.index(higher)]

def identifyHigherLevelTarget(self, data: bytes) -> HigherMultiplexableSIOResponder:
for el in self:
if el.ifThisResponder(data):
return el

def onReceive(self, lowerLevelTransport: SIOTransport, data: bytes):
higherLevelTransport = self.identifyHigherLevelTarget(data)
return higherLevelTransport.onReceive(self, data)

def sendBytes(self, data: bytes):
self.lower.sendBytes(data)


class HigherMultiplexableSIOResponder(SIOResponder):
"""A mixin class that defines the facilities needed to map raw bytes to upper levels"""

__slots__ = ()

NHI_1LO_MUX_MARKER_BYTES = None # type: bytes
NHI_1LO_MUX_MARKER_POSITION = None # type: slice

@classmethod
def getMarkerSlice(cls) -> slice:
return slice(cls.NHI_1LO_MUX_MARKER_POSITION, cls.NHI_1LO_MUX_MARKER_POSITION + len(cls.NHI_1LO_MUX_MARKER_BYTES))

def ifThisResponder(self, data: bytes):
slc = self.__class__.getMarkerSlice()
marker = data[slc]
return marker == self.__class__.NHI_1LO_MUX_MARKER_BYTES


class SIOIdentificationBytesNHi1LoMux(SIONHi1LoMux):
__slots__ = ("markBytesSlice",)

def __init__(self, markBytesSlice: slice):
self.markBytesSlice = markBytesSlice
self.higher = {}

def __iter__(self):
return self.higher.values()

def identifyHigherLevelTarget(self, data: bytes):
marker = data[self.markBytesSlice]
return self.higher[marker]

def bindHigher(self, higher: HigherMultiplexableSIOResponder):
assert higher.getMarkerSlice() != self.markBytesSlice, "This " + repr(higher) + " has incompatible location of marker slice: higher.getMarkerSlice() (" + higher.getMarkerSlice() + ") != self.markBytesSlice (" + self.markBytesSlice + ")"
self[higher.NHI_1LO_MUX_MARKER_BYTES] = higher

def unbindHigher(self, higher: HigherMultiplexableSIOResponder):
del self[higher.NHI_1LO_MUX_MARKER_BYTES]


class UnsafelyMuxableSIO1HiNLoMux(SIO1HiNLoMux):
"""A mux that can be muxed, but unsafely"""

def lowestLevelResourceID(self):
return id(self)


class HalfDuplex_SIOTransport(SIOTransport):
"""The SIOTransport that works on byte-by-byte. These protocols usually use in-band signalling, so we have to handle them. So we process the input byte-by-byte."""

__slots__ = ("tx",)

immediateSend = True
if not immediateSend:
warn("Some software requires the responses to be sent within a strict time windows (50 ms by default, but no more than 999). If we delay sending using the queue (immediateSend = False), we don't fit this time window.")

def __init__(self):
super().__init__()
self.tx = Queue()

def canSend(self) -> bool:
"""Determines if the remote party has finished its communication, so we can send own one"""
raise NotImplementedError

def sendCommandsInCertainStates(self):
# print("self.inCommand", self.inCommand)
if self.canSend():
while not self.tx.empty():
commandB = self.tx.get()
# print("commandB", commandB)
self.lower.sendBytes(commandB)
self.tx.task_done()
# print("written")

def sendBytes(self, data: bytes):
if self.__class__.immediateSend:
return self.lower.sendBytes(data)
else:
self.tx.put(data)
self.sendCommandsInCertainStates()


class StreamingHalfDuplexInBandSignalling_SIOTransport(HalfDuplex_SIOTransport):
__slots__ = ()

def receiveByte(self, b: int):
raise NotImplementedError

def filterSentBytes(self, b: bytes) -> bytes:
raise NotImplementedError

def onReceive(self, lowerLevelTransport: "SIOTransport", data: bytes):
for b in data:
self.receiveByte(b)

def sendBytes(self, data: bytes):
return super().sendBytes(self.filterSentBytes(data))
Loading

0 comments on commit 44f3015

Please sign in to comment.