Merge pull request #1 from rus07tam/release/v0.4.1

Release/v0.4.1
This commit is contained in:
rus07tam 2025-10-27 15:05:33 +03:00 committed by GitHub
commit 4f258ffc7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 103 additions and 678 deletions

1
.gitignore vendored
View file

@ -13,5 +13,6 @@ wheels/
.direnv .direnv
.mypy_cache .mypy_cache
.python-version .python-version
.pytest_cache
.vscode .vscode
_autosummary _autosummary

678
README.md
View file

@ -1,29 +1,41 @@
<div align="center">
# 🐍 Snakia Framework # 🐍 Snakia Framework
![Code Quality](https://img.shields.io/codacy/grade/508e02449dd145e7a93c7ba08ac756a4?logo=codacy)
![Code Size](https://img.shields.io/github/languages/code-size/ruject/snakia)
![License](https://img.shields.io/github/license/ruject/snakia)
![Open Issues](https://img.shields.io/github/issues-raw/ruject/snakia)
![Commit Activity](https://img.shields.io/github/commit-activity/m/ruject/snakia)
[API Reference](https://ruject.github.io/snakia/)
&nbsp;&nbsp;
[Telegram Chat](https://t.me/RuJect_Community)
</div>
**Snakia** is a modern Python framework for creating applications with Entity-Component-System (ECS) architecture, event system, and reactive programming. Built with performance (maybe) and modularity in mind, Snakia provides a clean API for developing complex applications ranging from games to terminal user interfaces. **Snakia** is a modern Python framework for creating applications with Entity-Component-System (ECS) architecture, event system, and reactive programming. Built with performance (maybe) and modularity in mind, Snakia provides a clean API for developing complex applications ranging from games to terminal user interfaces.
## 📋 Table of Contents ## 📋 Table of Contents
- [🎯 Roadmap & TODO](#-roadmap--todo) - [🐍 Snakia Framework](#-snakia-framework)
- [🚀 Installation](#-installation) - [📋 Table of Contents](#-table-of-contents)
- [🚀 Quick Start](#-quick-start) - [✨ Key Features](#-key-features)
- [🏗️ Architecture](#-architecture) - [🚀 Installation](#-installation)
- [⚙️ Core](#-core) - [Prerequisites](#prerequisites)
- [🎯 ECS System](#-ecs-system) - [Install from PyPi (recommended)](#install-from-pypi-recommended)
- [📡 Event System (ES)](#-event-system-es) - [Install from Source](#install-from-source)
- [🔌 Plugin System](#-plugin-system) - [🎯 Roadmap \& TODO](#-roadmap--todo)
- [🎨 TUI System](#-tui-system) - [🚀 Quick Start](#-quick-start)
- [⚡ Reactive Programming (RX)](#-reactive-programming-rx) - [🏗️ Architecture](#-architecture)
- [🛠️ Utilities](#-utilities) - [📦 Examples](#-examples)
- [🎭 Decorators](#-decorators) - [Health System](#health-system)
- [🏷️ Properties](#-properties) - [TUI Application](#tui-application)
- [🌐 Platform Abstraction](#-platform-abstraction) - [🤝 Contributing](#-contributing)
- [📦 Examples](#-examples) - [How to Contribute](#how-to-contribute)
- [🤝 Contributing](#-contributing) - [Development Guidelines](#development-guidelines)
- [🆘 Support](#-support)
- [📄 License](#-license)
### ✨ Key Features ## ✨ Key Features
- 🏗️ **ECS Architecture** - Flexible entity-component-system for scalable game/app logic - 🏗️ **ECS Architecture** - Flexible entity-component-system for scalable game/app logic
- 📡 **Event System** - Asynchronous event handling with filters and priorities - 📡 **Event System** - Asynchronous event handling with filters and priorities
@ -52,15 +64,9 @@ pip install snakia
### Install from Source ### Install from Source
```bash ```bash
# Clone the repository
git clone https://github.com/RuJect/Snakia.git git clone https://github.com/RuJect/Snakia.git
cd Snakia cd Snakia
# Install with pip
pip install -e . pip install -e .
# Or with uv (recommended)
uv sync
``` ```
## 🎯 Roadmap & TODO ## 🎯 Roadmap & TODO
@ -133,616 +139,6 @@ Snakia/
└── types/ # Special types └── types/ # Special types
``` ```
## ⚙️ Core
### Engine
The central component of the framework that coordinates all systems:
```python
from snakia.core.engine import Engine
engine = Engine()
# Systems:
# - engine.system - ECS system
# - engine.dispatcher - Event system
# - engine.loader - Plugin loader
engine.start() # Start all systems
engine.stop() # Stop all systems
engine.update() # Update systems
```
## 🎯 ECS System
Entity-Component-System architecture for creating flexible and performant applications.
### Component
Base class for all components:
```python
from snakia.core.ecs import Component
from pydantic import Field
class PositionComponent(Component):
x: float = Field(default=0.0)
y: float = Field(default=0.0)
class VelocityComponent(Component):
vx: float = Field(default=0.0)
vy: float = Field(default=0.0)
```
### Processor
Processors handle components in the system:
```python
from snakia.core.ecs import Processor, System
class MovementProcessor(Processor):
def process(self, system: System) -> None:
# Get all entities with Position and Velocity
for entity, (pos, vel) in system.get_components(
PositionComponent, VelocityComponent
):
pos.x += vel.vx
pos.y += vel.vy
```
### System
Entity and component management:
```python
# Creating an entity with components
entity = system.create_entity(
PositionComponent(x=10, y=20),
VelocityComponent(vx=1, vy=0)
)
# Adding a component to an existing entity
system.add_component(entity, HealthComponent(value=100))
# Getting entity components
pos, vel = system.get_components_of_entity(
entity, PositionComponent, VelocityComponent
)
# Checking for components
if system.has_components(entity, PositionComponent, VelocityComponent):
print("Entity has position and velocity")
# Removing a component
system.remove_component(entity, VelocityComponent)
# Deleting an entity
system.delete_entity(entity)
```
## 📡 Event System (ES)
Asynchronous event system with filter and priority support.
### Event
Base class for events:
```python
from snakia.core.es import Event
from pydantic import Field
class PlayerDiedEvent(Event):
player_id: int = Field()
cause: str = Field(default="unknown")
ttl: int = Field(default=10) # Event lifetime
```
### Handler
Event handlers:
```python
from snakia.core.es import Handler, Action
def on_player_died(event: PlayerDiedEvent) -> Action | None:
print(f"Player {event.player_id} died from {event.cause}")
return Action.move(1) # Move to next handler
```
### Filter
Event filters:
```python
from snakia.core.es import Filter
def only_important_deaths(event: PlayerDiedEvent) -> bool:
return event.cause in ["boss", "pvp"]
# Using a filter
@dispatcher.on(PlayerDiedEvent, filter=only_important_deaths)
def handle_important_death(event: PlayerDiedEvent):
print("Important death occurred!")
```
### Dispatcher
Central event dispatcher:
```python
from snakia.core.es import Dispatcher, Subscriber
dispatcher = Dispatcher()
# Subscribing to an event
dispatcher.subscribe(PlayerDiedEvent, Subscriber(
handler=on_player_died,
filter=only_important_deaths,
priority=10
))
# Decorator for subscription
@dispatcher.on(PlayerDiedEvent, priority=5)
def handle_death(event: PlayerDiedEvent):
print("Death handled!")
# Publishing an event
dispatcher.publish(PlayerDiedEvent(player_id=123, cause="boss"))
```
## 🔌 Plugin System
Modular system for loading and managing plugins.
### Plugin
Base class for plugins:
```python
from snakia.core.loader import Meta, Plugin, PluginProcessor
from snakia.types import Version
class MyProcessor(PluginProcessor):
def process(self, system):
# Processor logic
pass
class MyPlugin(Plugin, meta=Meta(
name="my_plugin",
author="developer",
version=Version.from_args(1, 0, 0),
processors=(MyProcessor,),
subscribers=()
)):
def on_load(self):
print("Plugin loaded!")
def on_unload(self):
print("Plugin unloaded!")
```
### Meta
Plugin metadata:
```python
from snakia.core.loader import Meta
from snakia.core.es import Subscriber
meta = Meta(
name="plugin_name",
author="author_name",
version=Version.from_args(1, 0, 0),
processors=(Processor1, Processor2),
subscribers=(
(EventType, Subscriber(handler, filter, priority)),
)
)
```
### Loader
Plugin loader:
```python
from snakia.core.loader import Loader
loader = Loader(engine)
# Registering a plugin
loader.register(MyPlugin)
# Loading all plugins
loader.load_all()
# Unloading all plugins
loader.unload_all()
```
## 🎨 TUI System
System for creating text-based user interfaces.
### Widget
Base class for widgets:
```python
from snakia.core.tui import Widget, Canvas, CanvasChar
from snakia.core.rx import Bindable
class MyWidget(Widget):
def __init__(self):
super().__init__()
self.text = self.state("Hello World")
self.color = self.state(CanvasChar(fg_color="red"))
def on_render(self) -> Canvas:
canvas = Canvas(20, 5)
canvas.draw_text(0, 0, self.text.value, self.color.value)
return canvas
```
### Canvas
Drawing canvas:
```python
from snakia.core.tui import Canvas, CanvasChar
canvas = Canvas(80, 24)
# Drawing text
canvas.draw_text(10, 5, "Hello", CanvasChar(fg_color="blue"))
# Drawing rectangle
canvas.draw_rect(0, 0, 20, 10, CanvasChar("█", fg_color="green"))
# Filling area
canvas.draw_filled_rect(5, 5, 10, 5, CanvasChar(" ", bg_color="red"))
# Lines
canvas.draw_line_h(0, 0, 20, CanvasChar("-"))
canvas.draw_line_v(0, 0, 10, CanvasChar("|"))
```
### CanvasChar
Character with attributes:
```python
from snakia.core.tui import CanvasChar
char = CanvasChar(
char="A",
fg_color="red", # Text color
bg_color="blue", # Background color
bold=True, # Bold
italic=False, # Italic
underline=True # Underline
)
```
### Renderer
Screen rendering:
```python
from snakia.core.tui import RenderContext
from snakia.core.tui.render import ANSIRenderer
import sys
class StdoutTarget:
def write(self, text: str): sys.stdout.write(text)
def flush(self): sys.stdout.flush()
renderer = ANSIRenderer(StdoutTarget())
with RenderContext(renderer) as ctx:
ctx.render(widget.render())
```
### Ready-made Widgets
```python
from snakia.core.tui.widgets import (
TextWidget, BoxWidget,
HorizontalSplitWidget, VerticalSplitWidget
)
# Text widget
text = TextWidget("Hello", CanvasChar(fg_color="red", bold=True))
# Box widget
box = BoxWidget(10, 5, CanvasChar("█", fg_color="yellow"))
# Splitters
h_split = HorizontalSplitWidget([text1, text2], "|")
v_split = VerticalSplitWidget([h_split, box], "-")
```
## ⚡ Reactive Programming (RX)
Reactive programming system for creating responsive interfaces.
### Bindable
Reactive variables:
```python
from snakia.core.rx import Bindable, ValueChanged
# Creating a reactive variable
counter = Bindable(0)
# Subscribing to changes
def on_change(event: ValueChanged[int]):
print(f"Counter changed from {event.old_value} to {event.new_value}")
counter.subscribe(on_change)
# Changing value
counter.set(5) # Will call on_change
counter(10) # Alternative syntax
```
### AsyncBindable
Asynchronous reactive variables:
```python
from snakia.core.rx import AsyncBindable
async_counter = AsyncBindable(0)
async def async_handler(event: ValueChanged[int]):
print(f"Async counter: {event.new_value}")
await async_counter.subscribe(async_handler, run_now=True)
await async_counter.set(42)
```
### Operators
```python
from snakia.core.rx import map, filter, combine, merge
# Transformation
doubled = map(counter, lambda x: x * 2)
# Filtering
even_only = filter(counter, lambda x: x % 2 == 0)
# Combining
combined = combine(counter, doubled, lambda a, b: a + b)
# Merging streams
merged = merge(counter, async_counter)
```
## 🛠️ Utilities
### to_async
Converting synchronous functions to asynchronous:
```python
from snakia.utils import to_async
def sync_function(x):
return x * 2
async_function = to_async(sync_function)
result = await async_function(5)
```
### nolock
Performance optimization:
```python
from snakia.utils import nolock
def busy_loop():
while running:
# Work
nolock() # Release GIL
```
### inherit
Simplified inheritance:
```python
from snakia.utils import inherit
class Base:
def method(self): pass
class Derived(inherit(Base)):
def method(self):
super().method()
# Additional logic
```
### this
Reference to current object:
```python
from snakia.utils import this
def func():
return this() # Returns `<function func at ...>`
```
### throw
Throwing exceptions:
```python
from snakia.utils import throw
def validate(value):
if value < 0:
throw(ValueError("Value must be positive"))
```
### frame
Working with frames:
```python
from snakia.utils import frame
def process_frame():
current_frame = frame()
# Process frame
```
## 🎭 Decorators
### inject_replace
Method replacement:
```python
from snakia.decorators import inject_replace
class Original:
def method(self): return "original"
@inject_replace(Original, "method")
def new_method(self): return "replaced"
```
### inject_before / inject_after
Hooks before and after execution:
```python
from snakia.decorators import inject_before, inject_after
@inject_before(MyClass, "method")
def before_hook(self): print("Before method")
@inject_after(MyClass, "method")
def after_hook(self): print("After method")
```
### singleton
Singleton pattern:
```python
from snakia.decorators import singleton
@singleton
class Database:
def __init__(self):
self.connection = "connected"
```
### pass_exceptions
Exception handling:
```python
from snakia.decorators import pass_exceptions
@pass_exceptions(ValueError, TypeError)
def risky_function():
# Code that might throw exceptions
pass
```
## 🏷️ Properties
### readonly
Read-only property:
```python
from snakia.property import readonly
class Currency:
@readonly
def rate(self) -> int:
return 100
currency = Currency()
currency.rate = 200
print(currency.rate) # Output: 100
```
### initonly
Initialization-only property:
```python
from snakia.property import initonly
class Person:
name = initonly[str]("name")
bob = Person()
bob.name = "Bob"
print(bob.name) # Output: "Bob"
bob.name = "not bob"
print(bob.name) # Output: "Bob"
```
### 🏛️ classproperty
Class property:
```python
from snakia.property import classproperty
class MyClass:
@classproperty
def class_value(cls):
return "class_value"
```
## 🌐 Platform Abstraction
### 🖥️ PlatformOS
Operating system abstraction:
```python
from snakia.platform import PlatformOS, OS
# Detecting current OS
current_os = OS.current()
if current_os == PlatformOS.LINUX:
print("Running on Linux")
elif current_os == PlatformOS.ANDROID:
print("Running on Android")
```
### 🏗️ PlatformLayer
Platform layers:
```python
from snakia.platform import LinuxLayer, AndroidLayer
# Linux layer
linux_layer = LinuxLayer()
# Android layer
android_layer = AndroidLayer()
```
## 📦 Examples ## 📦 Examples
### Health System ### Health System
@ -857,15 +253,3 @@ We welcome contributions to Snakia development! Whether you're fixing bugs, addi
- Write clear commit messages - Write clear commit messages
- Update documentation for new features - Update documentation for new features
- Test your changes thoroughly - Test your changes thoroughly
## 🆘 Support
Need help? We're here to assist you!
- 🐛 **Bug Reports** - [GitHub Issues](https://github.com/RuJect/Snakia/issues)
- 💬 **Community Chat** - [RuJect Community Telegram](https://t.me/RuJect_Community)
- 📧 **Direct Contact** - mailto:rus07tam.uwu@gmail.com
## 📄 License
See the `LICENSE` file for details.

View file

@ -1,6 +1,6 @@
[project] [project]
name = "snakia" name = "snakia"
version = "0.4.0" version = "0.4.1"
description = "Modern python framework" description = "Modern python framework"
readme = "README.md" readme = "README.md"
authors = [ authors = [
@ -38,3 +38,4 @@ disable = ["C0114", "C0115", "C0116", "R0801"]
max-args = 8 max-args = 8
max-positional-arguments = 7 max-positional-arguments = 7
min-public-methods = 1 min-public-methods = 1
fail-on = "error"

View file

@ -4,6 +4,7 @@ from .bindable import Bindable
from .chain import chain from .chain import chain
from .combine import combine from .combine import combine
from .concat import concat from .concat import concat
from .cond import cond
from .const import const from .const import const
from .filter import filter # noqa: W0622 # pylint: disable=W0622 from .filter import filter # noqa: W0622 # pylint: disable=W0622
from .map import map # noqa: W0622 # pylint: disable=W0622 from .map import map # noqa: W0622 # pylint: disable=W0622
@ -18,6 +19,7 @@ __all__ = [
"chain", "chain",
"combine", "combine",
"concat", "concat",
"cond",
"const", "const",
"filter", "filter",
"map", "map",

View file

@ -12,10 +12,6 @@ class AsyncBindable[T: Any](BaseBindable[T]):
super().__init__(default_value) super().__init__(default_value)
self.__subscribers: list[BindableSubscriber[T, Awaitable[Any]]] = [] self.__subscribers: list[BindableSubscriber[T, Awaitable[Any]]] = []
@property
def value(self) -> T:
return self.__value
@property @property
def subscribers( def subscribers(
self, self,
@ -25,8 +21,8 @@ class AsyncBindable[T: Any](BaseBindable[T]):
async def set(self, value: T) -> None: async def set(self, value: T) -> None:
"""Set the value.""" """Set the value."""
e = ValueChanged(self.__value, value) e = ValueChanged(self.value, value)
self.__value = value self.set_silent(value)
for subscriber in self.__subscribers: for subscriber in self.__subscribers:
await subscriber(e) await subscriber(e)
@ -58,7 +54,7 @@ class AsyncBindable[T: Any](BaseBindable[T]):
async def _run() -> None: async def _run() -> None:
await subscriber( await subscriber(
ValueChanged(self.__default_value, self.__value) ValueChanged(self.__default_value, self.value)
) )
return _run() return _run()
@ -96,7 +92,7 @@ class AsyncBindable[T: Any](BaseBindable[T]):
async def _run() -> None: async def _run() -> None:
await subscriber( await subscriber(
ValueChanged(self.__default_value, self.__value) ValueChanged(self.__default_value, self.value)
) )
return _run() return _run()

View file

@ -19,7 +19,7 @@ class Bindable[T: Any](BaseBindable[T]):
def set(self, value: T) -> None: def set(self, value: T) -> None:
"""Set the value.""" """Set the value."""
e = ValueChanged(self.__value, value) e = ValueChanged(self.value, value)
self.set_silent(value) self.set_silent(value)
for subscriber in self.__subscribers: for subscriber in self.__subscribers:
subscriber(e) subscriber(e)

View file

@ -0,0 +1,13 @@
from typing import Callable
def cond[**P, T, F](
condition: Callable[P, bool],
if_true: Callable[P, T],
if_false: Callable[P, F],
) -> Callable[P, T | F]:
return lambda *args, **kw: (
if_true(*args, **kw)
if condition(*args, **kw)
else if_false(*args, **kw)
)

View file

@ -36,14 +36,24 @@ class Field[T: Any](ABC, PrivProperty[T]):
@classmethod @classmethod
def custom[R]( def custom[R](
cls: type[Field[Any]], cls: type[Field[Any]],
serialize: Callable[[R], str], serialize: Callable[[Field[R], R], bytes],
deserialize: Callable[[str], R], deserialize: Callable[[Field[R], bytes], R],
) -> type[Field[R]]: ) -> type[Field[R]]:
return inherit( return inherit(
cls, {"serialize": serialize, "deserialize": deserialize} cls, {"serialize": serialize, "deserialize": deserialize}
) )
@final
@staticmethod
def get_fields(class_: type[Any] | Any, /) -> dict[str, Field[Any]]:
if not isinstance(class_, type):
class_ = class_.__class__
return {
k: v for k, v in class_.__dict__.items() if isinstance(v, Field)
}
if TYPE_CHECKING: if TYPE_CHECKING:
@final
@classmethod @classmethod
def type(cls) -> type[T]: ... def type(cls) -> type[T]: ...

View file

@ -1,5 +1,5 @@
from .cell_property import CellProperty from .cell_property import CellProperty
from .classproperty import ClassProperty from .classproperty import ClassProperty, classproperty
from .hook_property import HookProperty from .hook_property import HookProperty
from .initonly import Initonly, initonly from .initonly import Initonly, initonly
from .priv_property import PrivProperty from .priv_property import PrivProperty
@ -9,6 +9,7 @@ from .readonly import Readonly, readonly
__all__ = [ __all__ = [
"CellProperty", "CellProperty",
"ClassProperty", "ClassProperty",
"classproperty",
"HookProperty", "HookProperty",
"Initonly", "Initonly",
"initonly", "initonly",

View file

@ -48,7 +48,7 @@ class ClassProperty[T]:
def classproperty[T]( def classproperty[T](
fget: Callable[[Any], T] = empty.func, fget: Callable[[Any], T],
fset: Callable[[Any, T], None] = empty.func, fset: Callable[[Any, T], None] = empty.func,
fdel: Callable[[Any], None] = empty.func, fdel: Callable[[Any], None] = empty.func,
) -> ClassProperty[T]: ) -> ClassProperty[T]:

View file

@ -15,7 +15,7 @@ class PrivProperty[T]:
def __get__(self, instance: Any, owner: type | None = None, /) -> T: def __get__(self, instance: Any, owner: type | None = None, /) -> T:
if self.__default_value: if self.__default_value:
return getattr(instance, self.__name, self.__default_value) return getattr(instance, self.__name, self.__default_value)
return getattr(instance, self.__name) return getattr(instance, self.__name) # type: ignore
def __set__(self, instance: Any, value: T, /) -> None: def __set__(self, instance: Any, value: T, /) -> None:
setattr(instance, self.__name, value) setattr(instance, self.__name, value)

View file

@ -5,7 +5,12 @@ from snakia.types import empty
class Property[T]: class Property[T]:
""" """
A property that can be set, get, and deleted.""" A property that can be set, get, and deleted.
"""
__slots__ = "__fget", "__fset", "__fdel", "__name"
__name: str
def __init__( def __init__(
self, self,
@ -17,6 +22,9 @@ class Property[T]:
self.__fset = fset self.__fset = fset
self.__fdel = fdel self.__fdel = fdel
def __set_name__(self, owner: type, name: str) -> None:
self.__name = name
def __get__(self, instance: Any, owner: type | None = None, /) -> T: def __get__(self, instance: Any, owner: type | None = None, /) -> T:
return self.__fget(instance) return self.__fget(instance)
@ -40,3 +48,7 @@ class Property[T]:
"""Descriptor deleter.""" """Descriptor deleter."""
self.__fdel = fdel self.__fdel = fdel
return self return self
@property
def name(self) -> str:
return self.__name

View file

@ -1,27 +1,32 @@
from typing import Any, Callable from typing import Any, Callable
from snakia.utils import throw
class Readonly[T]: from .property import Property
class Readonly[T](Property[T]):
""" """
Readonly property. Readonly property.
""" """
__slots__ = ("__fget",)
def __init__( def __init__(
self, self,
fget: Callable[[Any], T], fget: Callable[[Any], T],
*,
strict: bool = False,
) -> None: ) -> None:
self.__fget = fget super().__init__(
fget=fget,
def __get__(self, instance: Any, owner: type | None = None, /) -> T: fset=(
return self.__fget(instance) (lambda *_: throw(TypeError("Cannot set readonly property")))
if strict
def __set__(self, instance: Any, value: T, /) -> None: else lambda *_: None
pass ),
)
def readonly[T](value: T) -> Readonly[T]: def readonly[T](value: T, *, strict: bool = False) -> Readonly[T]:
"""Create a readonly property with the given value. """Create a readonly property with the given value.
Args: Args:
@ -30,4 +35,4 @@ def readonly[T](value: T) -> Readonly[T]:
Returns: Returns:
Readonly[T]: The readonly property. Readonly[T]: The readonly property.
""" """
return Readonly(lambda _: value) return Readonly(lambda _: value, strict=strict)

2
uv.lock generated
View file

@ -104,7 +104,7 @@ wheels = [
[[package]] [[package]]
name = "snakia" name = "snakia"
version = "0.4.0" version = "0.4.1"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "networkx" }, { name = "networkx" },