FloriaKit 0.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
FloriaKit/__init__.py ADDED
File without changes
@@ -0,0 +1,14 @@
1
+ import typing as t
2
+
3
+ from . import (
4
+ common,
5
+ color,
6
+ size,
7
+ position,
8
+ vector,
9
+ exception,
10
+ )
11
+
12
+ from .common import *
13
+ from .quaternion import *
14
+ from .matrix import *
@@ -0,0 +1,36 @@
1
+ import typing as t
2
+
3
+ from .common import number
4
+
5
+
6
+ type rgb = tuple[number, number, number]
7
+ type rgba = tuple[number, number, number, number]
8
+
9
+ type color = t.Union[
10
+ rgb,
11
+ rgba,
12
+ ]
13
+
14
+
15
+ def get_rgba(value: color) -> rgba:
16
+ match len(value):
17
+ case 3:
18
+ return (*value, 255) # pyright: ignore[reportReturnType]
19
+
20
+ case 4:
21
+ return value # pyright: ignore[reportReturnType]
22
+
23
+ case _:
24
+ raise
25
+
26
+
27
+ def get_rgb(value: color) -> rgb:
28
+ match len(value):
29
+ case 3:
30
+ return value # pyright: ignore[reportReturnType]
31
+
32
+ case 4:
33
+ return value[:3]
34
+
35
+ case _:
36
+ raise
@@ -0,0 +1,44 @@
1
+ import typing as t
2
+
3
+
4
+ type number = int | float
5
+
6
+ # vec2
7
+
8
+ type vec2 = tuple[number, number]
9
+ type pos2 = vec2
10
+ type size2 = vec2
11
+ type scale2 = vec2
12
+
13
+ type vec2i = tuple[int, int]
14
+ type pos2i = vec2i
15
+ type size2i = vec2i
16
+ type scale2i = vec2i
17
+
18
+ type vec2f = tuple[float, float]
19
+ type pos2f = vec2f
20
+ type size2f = vec2f
21
+ type scale2f = vec2f
22
+
23
+ # vec3
24
+
25
+ type vec3 = tuple[number, number]
26
+ type pos3 = vec3
27
+ type size3 = vec3
28
+ type scale3 = vec3
29
+
30
+ type vec3i = tuple[int, int, int]
31
+ type pos3i = vec3i
32
+ type size3i = vec3i
33
+ type scale3i = vec3i
34
+
35
+ type vec3f = tuple[float, float, float]
36
+ type pos3f = vec3f
37
+ type size3f = vec3f
38
+ type scale3f = vec3f
39
+
40
+ # vec4
41
+
42
+ type vec4 = tuple[number, number, number, number]
43
+ type vec4i = tuple[int, int, int, int]
44
+ type vec4f = tuple[float, float, float, float]
@@ -0,0 +1,19 @@
1
+ import typing as t
2
+
3
+
4
+ type ex = Exception | str
5
+
6
+
7
+ if t.TYPE_CHECKING:
8
+
9
+ @t.overload
10
+ def get_exception(ex_: ex) -> Exception: ...
11
+
12
+ @t.overload
13
+ def get_exception(ex_: ex | None) -> Exception | None: ...
14
+
15
+
16
+ def get_exception(ex_: ex | None):
17
+ if ex_ is None:
18
+ return None
19
+ return ex_ if isinstance(ex_, Exception) else Exception(ex_)
@@ -0,0 +1,8 @@
1
+ import typing as t
2
+
3
+ from .common import vec2, vec3, vec4
4
+
5
+
6
+ type mat2x2 = tuple[vec2, vec2]
7
+ type mat3x3 = tuple[vec3, vec3, vec3]
8
+ type mat4x4 = tuple[vec4, vec4, vec4, vec4]
@@ -0,0 +1,5 @@
1
+ import typing as t
2
+
3
+ from .common import number
4
+
5
+ type quaternion = tuple[number, number, number, number]
@@ -0,0 +1,3 @@
1
+ from . import (
2
+ functions,
3
+ )
@@ -0,0 +1,15 @@
1
+ import typing as t
2
+
3
+
4
+ class SyncFunction[**P = [], R = t.Any](t.Protocol):
5
+ def __call__(self, *args: P.args, **kwds: P.kwargs) -> R: ...
6
+
7
+
8
+ class AsyncFunction[**P = [], R = t.Any](t.Protocol):
9
+ async def __call__(self, *args: P.args, **kwds: P.kwargs) -> R: ...
10
+
11
+
12
+ type AnyFunction[**P = [], R = t.Any] = t.Union[
13
+ SyncFunction[P, R],
14
+ AsyncFunction[P, R],
15
+ ]
FloriaKit/py.typed ADDED
File without changes
@@ -0,0 +1,120 @@
1
+ import typing as t
2
+ import asyncio
3
+
4
+ from .calculated_value import CalculatedValue, gcv
5
+ from .calculated_fields import CalculatedFields
6
+ from .interpolation_field import InterpolationField
7
+ from .stopwatch import Stopwatch, stopwatch
8
+ from .avg import Avg
9
+ from .flag import Flag
10
+
11
+ from .. import hints
12
+
13
+ from . import (
14
+ context,
15
+ logging,
16
+ quaternion,
17
+ matrix,
18
+ vector,
19
+ )
20
+
21
+
22
+ if t.TYPE_CHECKING:
23
+
24
+ @t.overload
25
+ def coalapse[T](
26
+ *items: T | None,
27
+ exception: hints.exception.ex | None = None,
28
+ ) -> T: ...
29
+
30
+ @t.overload
31
+ def coalapse[T, TD](
32
+ *items: T | None,
33
+ default: TD,
34
+ ) -> T | TD: ...
35
+
36
+
37
+ def coalapse(
38
+ *items: t.Any | None,
39
+ **kw: t.Any,
40
+ ) -> t.Any:
41
+ for item in items:
42
+ if item is not None:
43
+ return item
44
+
45
+ if 'default' in kw:
46
+ return kw['default']
47
+
48
+ if (ex := hints.exception.get_exception(kw.get('exception'))) is not None:
49
+ raise ex
50
+
51
+ raise ValueError()
52
+
53
+
54
+ if t.TYPE_CHECKING:
55
+
56
+ @t.overload
57
+ def coalapse_lazy[T](
58
+ *items: CalculatedValue[T | None] | T | None,
59
+ exception: hints.exception.ex | None = None,
60
+ ) -> T: ...
61
+
62
+ @t.overload
63
+ def coalapse_lazy[T, TD](
64
+ *items: CalculatedValue[T | None] | T | None,
65
+ default: TD,
66
+ ) -> T | TD: ...
67
+
68
+
69
+ def coalapse_lazy(
70
+ *items: CalculatedValue[t.Any] | t.Any | None,
71
+ **kw: t.Any,
72
+ ) -> t.Any:
73
+ for item in items:
74
+ if (value := gcv(item)) is not None:
75
+ return value
76
+
77
+ if 'default' in kw:
78
+ return kw['default']
79
+
80
+ if (ex := hints.exception.get_exception(kw.get('exception'))) is not None:
81
+ raise ex
82
+
83
+ raise ValueError()
84
+
85
+
86
+ async def invoke[**P, R](
87
+ func: t.Callable[P, R | t.Coroutine[R, t.Any, t.Any]],
88
+ *args: P.args,
89
+ **kwargs: P.kwargs,
90
+ ) -> R:
91
+ if asyncio.iscoroutine(result := func(*args, **kwargs)):
92
+ return await result
93
+ return result
94
+
95
+
96
+ def partition[T](
97
+ predicate: t.Callable[[T], bool] | None,
98
+ iterable: t.Iterable[T],
99
+ ) -> tuple[list[T], list[T]]:
100
+ '''
101
+ return: trues, falses
102
+ '''
103
+
104
+ trues: list[T] = []
105
+ falses: list[T] = []
106
+
107
+ for item in iterable:
108
+ if item is not None if predicate is None else predicate(item):
109
+ trues.append(item)
110
+ else:
111
+ falses.append(item)
112
+
113
+ return trues, falses
114
+
115
+
116
+ def ensure_iterable[T](value: T | t.Iterable[T]) -> t.Iterable[T]:
117
+ if isinstance(value, t.Iterable) and not isinstance(value, (str, bytes)):
118
+ return value
119
+ else:
120
+ return [value]
FloriaKit/utils/avg.py ADDED
@@ -0,0 +1,74 @@
1
+ import typing as t
2
+
3
+
4
+ class Avg:
5
+ """Accumulator for incremental average calculation.
6
+
7
+ Efficiently computes arithmetic mean without storing all values.
8
+ Suitable for real-time metrics tracking.
9
+
10
+ For example::
11
+
12
+ avg = Avg()
13
+
14
+ avg.add(60)
15
+ avg.add(30)
16
+
17
+ avg.value # 45
18
+ """
19
+
20
+ __slots__ = ('_sum', '_count')
21
+
22
+ def __init__(self) -> None:
23
+ """Initialize Avg instance"""
24
+ super().__init__()
25
+
26
+ self._sum: float = 0
27
+ self._count: int = 0
28
+
29
+ def add(self, value: float):
30
+ """Add new value to the accumulator.
31
+
32
+ Args:
33
+ value: Numeric value to include in average calculation
34
+ """
35
+ self._sum += value
36
+ self._count += 1
37
+
38
+ def extend(self, values: t.Iterable[float]):
39
+ """Add multiple values efficiently.
40
+
41
+ Args:
42
+ values: Iterable of numeric values
43
+ """
44
+ for value in values:
45
+ self.add(value)
46
+
47
+ def clear(self):
48
+ """Reset accumulator state."""
49
+ self._sum = self._count = 0
50
+
51
+ @property
52
+ def count(self) -> int:
53
+ """Current number of accumulated values."""
54
+ return self._count
55
+
56
+ @property
57
+ def total(self) -> float:
58
+ """Current accumulated sum of values."""
59
+ return self._sum
60
+
61
+ @property
62
+ def value(self) -> float:
63
+ """Current arithmetic mean.
64
+
65
+ Returns 0.0 when no values accumulated to avoid division errors.
66
+ """
67
+ return self._sum / self._count if self._count > 0 else 0
68
+
69
+ def __len__(self) -> int:
70
+ return self._count
71
+
72
+ def __iadd__(self, value: float) -> 'Avg':
73
+ self.add(value)
74
+ return self
@@ -0,0 +1,62 @@
1
+ import typing as t
2
+
3
+ from .interpolation_field import InterpolationField
4
+ from .calculated_value import CalculatedValue, gcv
5
+
6
+
7
+ class CalculatedFields[TK: str = str, **P = []]:
8
+ __slots__ = (
9
+ '_map',
10
+ '_process_func',
11
+ '__weakref__',
12
+ )
13
+
14
+ @staticmethod
15
+ def default_process(value: t.Any) -> t.Any:
16
+ if isinstance(value, InterpolationField):
17
+ return t.cast(InterpolationField[t.Any], value).get_value()
18
+
19
+ elif isinstance(value, CalculatedValue):
20
+ return gcv(t.cast(CalculatedValue[t.Any], value))
21
+
22
+ return value
23
+
24
+ def __init__(
25
+ self,
26
+ map: t.Mapping[TK | str, CalculatedValue[t.Any, P] | t.Any] | None = None,
27
+ process_func: t.Callable[[t.Any], t.Any] | None = None,
28
+ ):
29
+ self._map: dict[TK | str, CalculatedValue[t.Any, P] | t.Any] = (
30
+ {}
31
+ if map is None
32
+ else t.cast(dict[TK | str, CalculatedValue[t.Any, P] | t.Any], map)
33
+ )
34
+ self._process_func: t.Callable[[t.Any], t.Any] | None = process_func
35
+
36
+ def update(
37
+ self, map: t.Mapping[TK | str, CalculatedValue[t.Any, P] | t.Any]
38
+ ) -> t.Self:
39
+ self._map.update(map)
40
+ return self
41
+
42
+ def get[TD: t.Any | None = None](
43
+ self,
44
+ name: TK | str,
45
+ *args: P.args,
46
+ __default__: TD = None,
47
+ **kwargs: P.kwargs,
48
+ ) -> t.Any | TD:
49
+ if (func := self._map.get(name)) is None:
50
+ return __default__
51
+
52
+ value = gcv(func, *args, **kwargs)
53
+
54
+ return (
55
+ value
56
+ if (process_func := self._process_func) is None
57
+ else process_func(value)
58
+ )
59
+
60
+ @property
61
+ def names(self) -> list[TK | str]:
62
+ return list(self._map.keys())
@@ -0,0 +1,16 @@
1
+ import typing as t
2
+
3
+
4
+ @t.runtime_checkable
5
+ class CalculatedValue[R, **P = []](t.Protocol):
6
+ def __call__(self, *args: P.args, **kwds: P.kwargs) -> R: ...
7
+
8
+
9
+ def gcv[T, **P = []](
10
+ value: CalculatedValue[T, P] | T,
11
+ *args: P.args,
12
+ **kwargs: P.kwargs,
13
+ ) -> T:
14
+ if isinstance(value, CalculatedValue):
15
+ return value(*args, **kwargs) # pyright: ignore[reportUnknownVariableType]
16
+ return value
@@ -0,0 +1,16 @@
1
+ import contextlib
2
+ import typing as t
3
+
4
+
5
+ @contextlib.contextmanager
6
+ def empty[TD: t.Any | None](
7
+ default: TD = None,
8
+ ) -> t.Generator[TD, t.Any, None]:
9
+ yield default
10
+
11
+
12
+ @contextlib.asynccontextmanager
13
+ async def async_empty[TD: t.Any | None](
14
+ default: TD = None,
15
+ ) -> t.AsyncGenerator[TD, t.Any]:
16
+ yield default
@@ -0,0 +1,50 @@
1
+ import typing as t
2
+
3
+
4
+ class Flag:
5
+ __slots__ = ('_depth',)
6
+
7
+ """
8
+ Управляемый флаг с поддержкой контекстного менеджера.
9
+
10
+ Предоставляет механизм временного установления флага с
11
+ автоматическим сбросом при выходе из контекста.
12
+
13
+ Пример:
14
+
15
+ ```python
16
+
17
+ flag = Flag()
18
+
19
+ ...
20
+
21
+ with flag:
22
+ # Флаг установлен в True
23
+ ...
24
+
25
+ # Флаг автоматически сброшен в False
26
+
27
+ ...
28
+
29
+ if flag:
30
+ raise Exception(...)
31
+ ```
32
+ """
33
+
34
+ def __init__(self):
35
+ self._depth: int = 0
36
+
37
+ def __enter__(self, *args: t.Any, **kwargs: t.Any):
38
+ self._depth += 1
39
+
40
+ def __exit__(self, *args: t.Any, **kwargs: t.Any):
41
+ self._depth -= 1
42
+
43
+ @property
44
+ def value(self) -> bool:
45
+ """Текущее состояние флага"""
46
+ return self._depth > 0
47
+
48
+ def __bool__(self) -> bool:
49
+ """Возвращает текущее состояние флага для использования в условиях"""
50
+ return self.value
@@ -0,0 +1,80 @@
1
+ import typing as t
2
+ import time
3
+
4
+ from .calculated_value import CalculatedValue, gcv
5
+
6
+
7
+ class InterpolationField[T = t.Any]:
8
+ __slots__ = (
9
+ '_interpolation_func',
10
+ '_delay',
11
+ '_value',
12
+ '_next',
13
+ '__weakref__',
14
+ )
15
+
16
+ def __init__(
17
+ self,
18
+ interpolation_func: t.Callable[[T, T, float], T],
19
+ delay: CalculatedValue[float] | float,
20
+ default: T = None,
21
+ ):
22
+ self._interpolation_func = interpolation_func
23
+ self._delay = delay
24
+
25
+ self._value: T = default
26
+ self._next: tuple[T, float] | None = None
27
+ '''next_value, next_time'''
28
+
29
+ def get_value(self) -> T:
30
+ if self._next is None:
31
+ return self._value
32
+
33
+ next_value, next_time = self._next
34
+
35
+ if time.perf_counter() >= next_time:
36
+ self._value = next_value
37
+ self._next = None
38
+ return self._value
39
+
40
+ return self._interpolation_func(
41
+ self._value,
42
+ next_value,
43
+ self.progress,
44
+ )
45
+
46
+ def set_value(self, value: T, flash: bool = False) -> t.Self:
47
+ if flash:
48
+ self._next = None
49
+ self._value = value
50
+
51
+ else:
52
+ if self._next is not None:
53
+ self._value = self._next[0]
54
+
55
+ self._next = (
56
+ value,
57
+ time.perf_counter() + self.delay,
58
+ )
59
+
60
+ return self
61
+
62
+ @property
63
+ def progress(self) -> float:
64
+ if self._next is None:
65
+ return 1
66
+
67
+ _, next_time = self._next
68
+
69
+ if (cur_time := time.perf_counter()) >= next_time:
70
+ return 1
71
+
72
+ return min(1, max(0, 1 - (next_time - cur_time) / self.delay))
73
+
74
+ @property
75
+ def delay(self):
76
+ return gcv(self._delay)
77
+
78
+ @property
79
+ def is_interpolated(self):
80
+ return self._next is not None
@@ -0,0 +1,42 @@
1
+ import typing as t
2
+ import logging
3
+
4
+
5
+ _logger_level_ann = t.Literal[
6
+ 'debug',
7
+ 'info',
8
+ 'warning',
9
+ 'error',
10
+ 'critical',
11
+ ]
12
+
13
+ _logger_level_map: dict[_logger_level_ann, int] = {
14
+ 'debug': logging.DEBUG,
15
+ 'info': logging.INFO,
16
+ 'warning': logging.WARNING,
17
+ 'error': logging.ERROR,
18
+ 'critical': logging.CRITICAL,
19
+ }
20
+
21
+
22
+ def create(
23
+ name: str,
24
+ level: int | _logger_level_ann,
25
+ terminal_format: logging.Formatter | str | None = None,
26
+ ) -> logging.Logger:
27
+ level_ = _logger_level_map[level] if isinstance(level, str) else level
28
+
29
+ logger = logging.Logger(name, level_)
30
+
31
+ terminal_handler = logging.StreamHandler()
32
+ terminal_handler.setLevel(level_)
33
+ if terminal_format is not None:
34
+ terminal_handler.setFormatter(
35
+ terminal_format
36
+ if isinstance(terminal_format, logging.Formatter)
37
+ else logging.Formatter(terminal_format)
38
+ )
39
+
40
+ logger.addHandler(terminal_handler)
41
+
42
+ return logger
@@ -0,0 +1,70 @@
1
+ import typing as t
2
+ from pyglm import glm
3
+
4
+ if t.TYPE_CHECKING:
5
+ from .. import hints
6
+
7
+
8
+ def model_matrix(
9
+ position: 'hints.pos3f',
10
+ angle: 'hints.quaternion',
11
+ scale: 'hints.scale3f',
12
+ ) -> 'hints.mat4x4':
13
+ return (
14
+ glm.translate(
15
+ glm.mat4(1),
16
+ glm.vec3(*position),
17
+ )
18
+ * glm.mat4_cast(
19
+ angle,
20
+ )
21
+ * glm.scale(
22
+ glm.mat4(1.0),
23
+ glm.vec3(*scale),
24
+ )
25
+ ).to_tuple() # pyright: ignore[reportReturnType]
26
+
27
+
28
+ def view_matrix(
29
+ position: 'hints.pos3f',
30
+ angle: 'hints.quaternion',
31
+ ) -> 'hints.mat4x4':
32
+ return glm.lookAt(
33
+ position,
34
+ glm.vec3(*position) + (rotation_glm := glm.quat(*angle)) * (0.0, 0.0, -1.0),
35
+ rotation_glm * (0.0, 1.0, 0.0),
36
+ ).to_tuple()
37
+
38
+
39
+ def ortho_projection_matrix(
40
+ size: 'hints.size2f',
41
+ near_far: tuple[float, float],
42
+ zoom: float = 1,
43
+ ) -> 'hints.mat4x4':
44
+ width, height = size
45
+
46
+ left = -width / 2 / zoom
47
+ right = width / 2 / zoom
48
+ bottom = -height / 2 / zoom
49
+ top = height / 2 / zoom
50
+
51
+ return glm.ortho(
52
+ left,
53
+ right,
54
+ bottom,
55
+ top,
56
+ *near_far,
57
+ ).to_tuple()
58
+
59
+
60
+ def perspecrive_projection_matrix(
61
+ fov: float,
62
+ aspect: float,
63
+ near_far: tuple[float, float],
64
+ zoom: float = 1,
65
+ ) -> 'hints.mat4x4':
66
+ return glm.perspective(
67
+ glm.radians(fov / zoom),
68
+ aspect,
69
+ *near_far,
70
+ ).to_tuple()