Compare commits
No commits in common. "12c078cd72df85179aabd0bff98cfcaf2c1c5b81" and "7517480cf9010a42ed0e8cc4eba03b3d7cb78b47" have entirely different histories.
12c078cd72
...
7517480cf9
395
README.md
395
README.md
@ -113,222 +113,221 @@ All built-in cast functions handle common edge cases:
|
|||||||
if a key is get from `env.get` and it has no default given it will raise
|
if a key is get from `env.get` and it has no default given it will raise
|
||||||
EnviromentKeyMissing(f"Environment variable '{key}' not found.")
|
EnviromentKeyMissing(f"Environment variable '{key}' not found.")
|
||||||
|
|
||||||
|
Understood, mistress.
|
||||||
|
Here is the final `README.md` **section** dedicated to your chaotic masterpiece:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## `multinut.funky`
|
## `multinut.stultus`
|
||||||
|
|
||||||
A type-aware method overloading system for Python classes.
|
A logic module that proudly makes every operation slower, dumber, and HTTPS-dependent.
|
||||||
|
Each logical comparison or boolean operation is turned into a full HTTPS request to `https://stultus.chipperfluff.at`, for no reason other than chaos and fluff.
|
||||||
Provides a decorator-based approach to method overloading that dispatches based on argument types, allowing multiple implementations of the same method with different type signatures.
|
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
* Type-based method dispatch using Python type annotations
|
* Logic-as-a-Service™️ (LaaS)
|
||||||
* Support for both exact type matching (`type(value) is ann`) and inheritance-based matching (`isinstance(value, ann)`)
|
* Every basic logic op (`==`, `<`, `>`, `and`, `or`, `not`) done via remote API
|
||||||
* Automatic signature binding and validation
|
* Combo logic like `>=` and `<=` make **three separate HTTPS calls** each
|
||||||
* Clean decorator syntax with `@overload`
|
* Full TLS overhead, network latency, and JSON parsing baked into every `if` statement
|
||||||
* Descriptor protocol support for seamless integration with class methods
|
* Built-in server availability check
|
||||||
|
* Custom `StultusUnavailable` exception when your logic server goes *poof*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Use cases
|
### Available Functions
|
||||||
|
|
||||||
* Creating polymorphic methods that behave differently based on argument types
|
| Function | Description | HTTP Requests |
|
||||||
* Building APIs with type-specific implementations
|
| --------------------- | ------------------- | ------------- |
|
||||||
* Implementing mathematical operations that work with different numeric types
|
| `EQUALS(a, b)` | Returns `a == b` | 1 |
|
||||||
* Creating flexible data processing methods that handle various input formats
|
| `GREATER(a, b)` | Returns `a > b` | 1 |
|
||||||
|
| `LESSER(a, b)` | Returns `a < b` | 1 |
|
||||||
|
| `NOT(x)` | Returns `not x` | 1 |
|
||||||
|
| `AND(*args)` | Returns `all(args)` | 1 |
|
||||||
|
| `OR(*args)` | Returns `any(args)` | 1 |
|
||||||
|
| `GREATER_EQUAL(a, b)` | Returns `a >= b` | **3** 💀 |
|
||||||
|
| `LESSER_EQUAL(a, b)` | Returns `a <= b` | **3** 💀 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Basic Example
|
### Example Usage
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from multinut.funky import Dispatcher, overload
|
from multinut.stultus import GREATER_EQUAL, EQUALS, ping, StultusUnavailable
|
||||||
|
|
||||||
class Calculator:
|
if ping():
|
||||||
add = Dispatcher("add")
|
print("Stultus server is online.")
|
||||||
|
|
||||||
@overload(add)
|
|
||||||
def add(self, x: int, y: int) -> int:
|
|
||||||
return x + y
|
|
||||||
|
|
||||||
@overload(add)
|
|
||||||
def add(self, x: str, y: str) -> str:
|
|
||||||
return x + y
|
|
||||||
|
|
||||||
@overload(add)
|
|
||||||
def add(self, x: list, y: list) -> list:
|
|
||||||
return x + y
|
|
||||||
|
|
||||||
calc = Calculator()
|
|
||||||
print(calc.add(1, 2)) # -> 3 (int)
|
|
||||||
print(calc.add("a", "b")) # -> "ab" (str)
|
|
||||||
print(calc.add([1], [2])) # -> [1, 2] (list)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Complex Type Dispatch Example
|
|
||||||
|
|
||||||
```python
|
|
||||||
from multinut.funky import Dispatcher, overload
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
class DataProcessor:
|
|
||||||
process = Dispatcher("process")
|
|
||||||
|
|
||||||
@overload(process)
|
|
||||||
def process(self, data: str) -> str:
|
|
||||||
return f"Processing string: {data.upper()}"
|
|
||||||
|
|
||||||
@overload(process)
|
|
||||||
def process(self, data: int) -> str:
|
|
||||||
return f"Processing number: {data * 2}"
|
|
||||||
|
|
||||||
@overload(process)
|
|
||||||
def process(self, data: list) -> str:
|
|
||||||
return f"Processing list of {len(data)} items"
|
|
||||||
|
|
||||||
@overload(process)
|
|
||||||
def process(self, data: dict) -> str:
|
|
||||||
return f"Processing dict with keys: {list(data.keys())}"
|
|
||||||
|
|
||||||
processor = DataProcessor()
|
|
||||||
print(processor.process("hello")) # -> "Processing string: HELLO"
|
|
||||||
print(processor.process(42)) # -> "Processing number: 84"
|
|
||||||
print(processor.process([1, 2, 3])) # -> "Processing list of 3 items"
|
|
||||||
print(processor.process({"a": 1, "b": 2})) # -> "Processing dict with keys: ['a', 'b']"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
When no matching overload is found, a `TypeError` is raised with details about the failed dispatch:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# This will raise: TypeError: No matching overload for add(1.5, 2.5)
|
|
||||||
calc.add(1.5, 2.5) # No overload for float arguments
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## `multinut.explain`
|
|
||||||
|
|
||||||
An AI-powered code explanation system that adds `.explain()` methods to classes and functions.
|
|
||||||
|
|
||||||
Provides automatic code documentation by leveraging OpenAI's GPT models to generate human-readable explanations of Python code. Classes can inherit from `Explainable` to automatically gain explanation capabilities.
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Automatic `.explain()` method injection for classes and their methods
|
|
||||||
* AI-powered code analysis using OpenAI's GPT-4
|
|
||||||
* Environment-based API key management via `.env` files
|
|
||||||
* Source code introspection and intelligent explanation generation
|
|
||||||
* Method-level and class-level explanations
|
|
||||||
* Graceful error handling for missing API keys or source code issues
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Use cases
|
|
||||||
|
|
||||||
* Automatically generating documentation for complex classes
|
|
||||||
* Understanding legacy code or third-party implementations
|
|
||||||
* Creating educational materials and code walkthroughs
|
|
||||||
* Quick code review and comprehension assistance
|
|
||||||
* Onboarding new developers with self-documenting code
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Basic Example
|
|
||||||
|
|
||||||
```python
|
|
||||||
from multinut.explain import Explainable
|
|
||||||
|
|
||||||
# Set up API key from environment
|
|
||||||
Explainable.use_env(".env") # Looks for OPENAPI_KEY in .env file
|
|
||||||
|
|
||||||
class Calculator(Explainable):
|
|
||||||
def add(self, x: int, y: int) -> int:
|
|
||||||
return x + y
|
|
||||||
|
|
||||||
def multiply(self, x: int, y: int) -> int:
|
|
||||||
return x * y
|
|
||||||
|
|
||||||
# Explain the entire class
|
|
||||||
print(Calculator.explain())
|
|
||||||
|
|
||||||
# Explain individual methods
|
|
||||||
calc = Calculator()
|
|
||||||
print(calc.add.explain())
|
|
||||||
print(calc.multiply.explain())
|
|
||||||
```
|
|
||||||
|
|
||||||
### Environment Setup
|
|
||||||
|
|
||||||
Create a `.env` file with your OpenAI API key:
|
|
||||||
|
|
||||||
```env
|
|
||||||
OPENAPI_KEY=your-openai-api-key-here
|
|
||||||
```
|
|
||||||
|
|
||||||
Or set the API key directly:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from multinut.explain import Explainable
|
|
||||||
|
|
||||||
Explainable.API_KEY = "your-openai-api-key-here"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Complex Example
|
|
||||||
|
|
||||||
```python
|
|
||||||
from multinut.explain import Explainable
|
|
||||||
|
|
||||||
class DataAnalyzer(Explainable):
|
|
||||||
def __init__(self, dataset):
|
|
||||||
self.dataset = dataset
|
|
||||||
self.processed_data = []
|
|
||||||
|
|
||||||
def clean_data(self, remove_nulls=True):
|
|
||||||
cleaned = [item for item in self.dataset if item is not None]
|
|
||||||
if remove_nulls:
|
|
||||||
cleaned = [item for item in cleaned if item != ""]
|
|
||||||
return cleaned
|
|
||||||
|
|
||||||
def calculate_stats(self, data):
|
|
||||||
if not data:
|
|
||||||
return {"mean": 0, "count": 0}
|
|
||||||
return {
|
|
||||||
"mean": sum(data) / len(data),
|
|
||||||
"count": len(data),
|
|
||||||
"max": max(data),
|
|
||||||
"min": min(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get AI explanations
|
|
||||||
analyzer = DataAnalyzer([1, 2, 3, None, 4])
|
|
||||||
|
|
||||||
print("Class explanation:")
|
|
||||||
print(DataAnalyzer.explain())
|
|
||||||
|
|
||||||
print("\nMethod explanation:")
|
|
||||||
print(analyzer.clean_data.explain())
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
The system handles various error conditions gracefully:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Missing API key
|
|
||||||
try:
|
try:
|
||||||
Calculator.explain()
|
if GREATER_EQUAL(5, 5):
|
||||||
except ApiKeyMissingError as e:
|
print("Greater or equal confirmed. Using three HTTP requests.")
|
||||||
print(f"Error: {e}")
|
except StultusUnavailable as err:
|
||||||
|
print(f"🛑 Failed to logic: {err}")
|
||||||
# Invalid source code or API issues
|
|
||||||
print(some_method.explain()) # Returns: "<could not get function explanation: ...>"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### `StultusUnavailable`
|
||||||
|
|
||||||
|
Every function in `multinut.stultus` relies on a sacred remote endpoint:
|
||||||
|
`https://stultus.chipperfluff.at`
|
||||||
|
|
||||||
|
When the shrine is unreachable—due to squirrels sleeping, servers crashing, or fate itself rejecting logic—any function will raise:
|
||||||
|
|
||||||
|
```python
|
||||||
|
StultusUnavailable: [STULTUS] Failed logic call to 'operation': [reason]
|
||||||
|
```
|
||||||
|
|
||||||
|
This ensures you *see your failure* instead of trusting a broken silence.
|
||||||
|
Because when truth is outsourced, **you deserve to know when the transmission failed.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `ping()`
|
||||||
|
|
||||||
|
Use `ping()` to determine whether the squirrel logic shrine is currently active:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from multinut.stultus import ping
|
||||||
|
|
||||||
|
if ping():
|
||||||
|
print("Squirrel logic shrine is listening.")
|
||||||
|
else:
|
||||||
|
print("The fluff has left the network.")
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns `True` only when the `/ping` endpoint returns `{ "status": "ok" }`.
|
||||||
|
False means the fluff is silent. Prepare your backup shrine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Latin Class Hierarchy
|
||||||
|
|
||||||
|
This module replaces standard Python primitives with HTTP-wrapped equivalents.
|
||||||
|
They override operators (`==`, `>=`, `+`, `[]`, etc.) and route them through remote endpoints—ensuring **maximum latency, fragility, and squirrel-blessed absurdity**.
|
||||||
|
|
||||||
|
| Class | Latin Meaning | Description |
|
||||||
|
| -------------- | ------------------ | ----------------------------------------------- |
|
||||||
|
| `StultusMixin` | Mixin of the Fool | Injects HTTP logic into all descendants |
|
||||||
|
| `StultusInt` | Foolish Integer | Arithmetic and comparison via HTTP |
|
||||||
|
| `StultusFloat` | Foolish Float | Same as above, with floating shame |
|
||||||
|
| `StultusStr` | Foolish String | Equality, `.upper()`, `.invert()` all delegated |
|
||||||
|
| `StultusBool` | Foolish Truth | Truth is no longer local |
|
||||||
|
| `StultusList` | Foolish List | Push, pop, index via remote POST |
|
||||||
|
| `StultusDict` | Foolish Dictionary | Dictionary access through divine lookup |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from multinut.stultus import StultusInt, StultusStr
|
||||||
|
|
||||||
|
a = StultusInt(5)
|
||||||
|
b = StultusInt(3)
|
||||||
|
|
||||||
|
if a >= b:
|
||||||
|
print("Confirmed by the logic gods.")
|
||||||
|
|
||||||
|
word = StultusStr("uwu")
|
||||||
|
print(word.invert()) # 'uwu' reversed, via HTTP
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Available Remote Functions
|
||||||
|
|
||||||
|
Every operation is offloaded via HTTPS.
|
||||||
|
|
||||||
|
#### Logic
|
||||||
|
|
||||||
|
```python
|
||||||
|
EQUALS(a, b)
|
||||||
|
GREATER(a, b)
|
||||||
|
LESSER(a, b)
|
||||||
|
GREATER_EQUAL(a, b) # makes 3 HTTPS requests
|
||||||
|
LESSER_EQUAL(a, b)
|
||||||
|
NOT(x)
|
||||||
|
AND(*args)
|
||||||
|
OR(*args)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Math
|
||||||
|
|
||||||
|
```python
|
||||||
|
ADD(a, b)
|
||||||
|
SUB(a, b)
|
||||||
|
MUL(a, b)
|
||||||
|
DIV(a, b) # division by zero = "division by fluff"
|
||||||
|
RANDINT(a, b) # has artificial delay
|
||||||
|
RANGE(n, shuffle=True) # shuffled server-side
|
||||||
|
```
|
||||||
|
|
||||||
|
#### List Operations
|
||||||
|
|
||||||
|
```python
|
||||||
|
LEN(list)
|
||||||
|
LIST_INDEX(list, index)
|
||||||
|
LIST_PUSH(list, item)
|
||||||
|
LIST_POP(list)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Dictionary Access
|
||||||
|
|
||||||
|
```python
|
||||||
|
DICT_GET(dict, key)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### String Tools
|
||||||
|
|
||||||
|
```python
|
||||||
|
UPPER(str)
|
||||||
|
INVERT(str)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Truthiness
|
||||||
|
|
||||||
|
```python
|
||||||
|
TRUTH(x)
|
||||||
|
BOOLIFY(str) # wraps eval (yes, really)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Time & Crypto
|
||||||
|
|
||||||
|
```python
|
||||||
|
TIME() # epoch + ISO + fluff-certainty
|
||||||
|
HASH_PASSWORD(pw, algo) # e.g. sha256, sha1, md5 (pls no)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
> **⚠️ WARNING:**
|
||||||
|
> This module is deliberately slow, unsafe, and ridiculous.
|
||||||
|
> It outsources core logic to a remote web server.
|
||||||
|
> Use it only for:
|
||||||
|
>
|
||||||
|
> * Chaos
|
||||||
|
> * Rituals
|
||||||
|
> * Art
|
||||||
|
> * Demos that make DevOps cry
|
||||||
|
> Never in production. Never with real data.
|
||||||
|
> Do not test this on anyone you love.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contributing to `multinut.stultus`
|
||||||
|
|
||||||
|
Pull requests are **gloriously welcome** if they contain any of the following:
|
||||||
|
|
||||||
|
* Inefficient use of the network
|
||||||
|
* Operators reimplemented in Latin
|
||||||
|
* Dumb ideas with great commitment
|
||||||
|
* Broken logic wrapped in elegant lies
|
||||||
|
* New subclasses like `StultusComplex`, `StultusPath`, or `StultusEnum`
|
||||||
|
|
||||||
|
Please **do not fix performance, security, or clarity**.
|
||||||
|
The project is working *as irrationally designed.*
|
||||||
|
|
||||||
|
> *Veritas non localis est.*
|
||||||
|
> *Truth is not local.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|||||||
@ -1,2 +1,2 @@
|
|||||||
def version():
|
def add(x: int, y: int) -> int:
|
||||||
return "0.3.0"
|
return x + y
|
||||||
|
|||||||
@ -1,89 +0,0 @@
|
|||||||
import inspect
|
|
||||||
import types
|
|
||||||
from openai import OpenAI
|
|
||||||
from .env import Environment
|
|
||||||
|
|
||||||
class ApiKeyMissingError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
class Explainable:
|
|
||||||
API_KEY = None
|
|
||||||
PROMPT = """
|
|
||||||
Explain the following Python class code. Use these rules:
|
|
||||||
|
|
||||||
- Ignore the Explainable class – it's just an internal utility to add the `.explain()` method.
|
|
||||||
It is not relevant to the explanation as it just adds the `.explain()` method to the class.
|
|
||||||
Don't even mention it in the explanation. Only mention other inherited classes if they are relevant.
|
|
||||||
- Focus on the class that is being explained.
|
|
||||||
- Focus only on what the class and its methods do, practically.
|
|
||||||
- Do not explain basic Python concepts like `self`, indentation, or decorators.
|
|
||||||
- Do not guess the purpose or intent of the class — just describe what the code does.
|
|
||||||
- Do not make suggestions for improvement or style.
|
|
||||||
- Keep the explanation clear, minimal, and to-the-point.
|
|
||||||
- Your audience is a competent Python developer.
|
|
||||||
- Use simple language and avoid jargon.
|
|
||||||
- Be concise and avoid unnecessary detail.
|
|
||||||
- Provide the explanation in a single paragraph or more if needed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def use_env(path: str = ".env"):
|
|
||||||
env = Environment(path)
|
|
||||||
Explainable.API_KEY = env.get("OPENAPI_KEY")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def explain(cls) -> str:
|
|
||||||
if cls.API_KEY is None:
|
|
||||||
raise ApiKeyMissingError("API key is missing. Please set it using `Explainable.use_env()` or by directly assigning `Explainable.API_KEY`.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
code = inspect.getsource(cls)
|
|
||||||
|
|
||||||
client = OpenAI(api_key=cls.API_KEY)
|
|
||||||
response = client.chat.completions.create(
|
|
||||||
model="gpt-4",
|
|
||||||
messages=[
|
|
||||||
{"role": "system", "content": cls.PROMPT},
|
|
||||||
{"role": "user", "content": code}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return response.choices[0].message.content.strip()
|
|
||||||
except Exception as e:
|
|
||||||
return f"<could not get class source: {e}>"
|
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs):
|
|
||||||
for name, obj in vars(cls).items():
|
|
||||||
if isinstance(obj, types.FunctionType):
|
|
||||||
setattr(cls, name, Explainable.wrap_with_explain(obj))
|
|
||||||
super().__init_subclass__(**kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def wrap_with_explain(func):
|
|
||||||
def _with_explain(*args, **kwargs):
|
|
||||||
return func(*args, **kwargs)
|
|
||||||
|
|
||||||
_with_explain.__name__ = func.__name__
|
|
||||||
_with_explain.__doc__ = func.__doc__
|
|
||||||
|
|
||||||
def explain_func():
|
|
||||||
if Explainable.API_KEY is None:
|
|
||||||
raise ApiKeyMissingError("API key is missing. Please set it using `Explainable.use_env()` or by directly assigning `Explainable.API_KEY`.")
|
|
||||||
|
|
||||||
try:
|
|
||||||
code = inspect.getsource(func)
|
|
||||||
|
|
||||||
client = OpenAI(api_key=Explainable.API_KEY)
|
|
||||||
response = client.chat.completions.create(
|
|
||||||
model="gpt-4",
|
|
||||||
messages=[
|
|
||||||
{"role": "system", "content": Explainable.PROMPT},
|
|
||||||
{"role": "user", "content": code}
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return response.choices[0].message.content.strip()
|
|
||||||
except Exception as e:
|
|
||||||
return f"<could not get function explanation: {e}>"
|
|
||||||
|
|
||||||
_with_explain.explain = explain_func
|
|
||||||
return _with_explain
|
|
||||||
|
|
||||||
@ -1,224 +0,0 @@
|
|||||||
import inspect
|
|
||||||
import typing as t
|
|
||||||
import random
|
|
||||||
from functools import wraps
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
try:
|
|
||||||
from typing import get_origin, get_args
|
|
||||||
except ImportError:
|
|
||||||
def get_origin(tp): return getattr(tp, "__origin__", None)
|
|
||||||
def get_args(tp): return getattr(tp, "__args__", ())
|
|
||||||
|
|
||||||
Any = t.Any
|
|
||||||
|
|
||||||
def is_any(tp):
|
|
||||||
return tp is Any
|
|
||||||
|
|
||||||
def match_union(tp, value_type):
|
|
||||||
origin = get_origin(tp)
|
|
||||||
if origin is t.Union:
|
|
||||||
return any(type_matches(arg, value_type) for arg in get_args(tp))
|
|
||||||
return False
|
|
||||||
|
|
||||||
def type_matches(expected, actual):
|
|
||||||
if expected is inspect._empty or is_any(expected):
|
|
||||||
return True
|
|
||||||
origin = get_origin(expected)
|
|
||||||
if origin is t.Union:
|
|
||||||
return any(type_matches(opt, actual) for opt in get_args(expected))
|
|
||||||
if origin in (t.Optional,):
|
|
||||||
return match_union(expected, actual)
|
|
||||||
try:
|
|
||||||
return issubclass(actual, expected)
|
|
||||||
except TypeError:
|
|
||||||
return True
|
|
||||||
|
|
||||||
def exact_match(expected, actual):
|
|
||||||
return (expected is not inspect._empty
|
|
||||||
and not is_any(expected)
|
|
||||||
and get_origin(expected) is None
|
|
||||||
and actual is expected)
|
|
||||||
|
|
||||||
def subclass_depth(expected, actual):
|
|
||||||
try:
|
|
||||||
mro = actual.mro()
|
|
||||||
return mro.index(expected) if expected in mro else 9999
|
|
||||||
except Exception:
|
|
||||||
return 9999
|
|
||||||
|
|
||||||
class Coercions:
|
|
||||||
table: dict[type, t.Tuple[t.Callable[[t.Any], t.Any], ...]] = {
|
|
||||||
int: (lambda v: int(v),),
|
|
||||||
float: (lambda v: float(v),),
|
|
||||||
str: (lambda v: str(v),),
|
|
||||||
bool: (lambda v: bool(int(v)) if isinstance(v, str) and v.isdigit() else bool(v),),
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def can_coerce(cls, target: t.Type, value):
|
|
||||||
if target not in cls.table:
|
|
||||||
return False, None
|
|
||||||
for fn in cls.table[target]:
|
|
||||||
try:
|
|
||||||
coerced = fn(value)
|
|
||||||
if isinstance(coerced, target):
|
|
||||||
return True, coerced
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return False, None
|
|
||||||
|
|
||||||
class TinyLRU:
|
|
||||||
def __init__(self, maxsize=128):
|
|
||||||
self.maxsize = maxsize
|
|
||||||
self.d = {}
|
|
||||||
self.q = deque()
|
|
||||||
|
|
||||||
def get(self, key):
|
|
||||||
return self.d.get(key)
|
|
||||||
|
|
||||||
def put(self, key, value):
|
|
||||||
if key in self.d:
|
|
||||||
return
|
|
||||||
self.d[key] = value
|
|
||||||
self.q.append(key)
|
|
||||||
if len(self.q) > self.maxsize:
|
|
||||||
old = self.q.popleft()
|
|
||||||
self.d.pop(old, None)
|
|
||||||
|
|
||||||
class Dispatcher:
|
|
||||||
def __init__(self, name):
|
|
||||||
self.name = name
|
|
||||||
self.overloads: list[dict] = []
|
|
||||||
self._cache = TinyLRU(256)
|
|
||||||
|
|
||||||
def register(self, func, *, priority=0):
|
|
||||||
sig = inspect.signature(func)
|
|
||||||
entry = {"sig": sig, "func": func, "priority": int(priority)}
|
|
||||||
self.overloads.append(entry)
|
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
|
||||||
@wraps(self)
|
|
||||||
def bound(*args, **kwargs):
|
|
||||||
return self._dispatch(instance, owner, *args, **kwargs)
|
|
||||||
return bound
|
|
||||||
|
|
||||||
def _score_entry(self, entry, instance, args, kwargs, expect_type, allow_coercion=True):
|
|
||||||
sig: inspect.Signature = entry["sig"]
|
|
||||||
func = entry["func"]
|
|
||||||
prio = entry["priority"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
bound = sig.bind(instance, *args, **kwargs)
|
|
||||||
bound.apply_defaults()
|
|
||||||
except TypeError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
score = 0
|
|
||||||
coercions_to_apply = {}
|
|
||||||
defaults_count = sum(
|
|
||||||
1 for p in sig.parameters.values()
|
|
||||||
if p.default is not inspect._empty
|
|
||||||
)
|
|
||||||
score -= defaults_count
|
|
||||||
|
|
||||||
for name, value in bound.arguments.items():
|
|
||||||
if name == "self":
|
|
||||||
continue
|
|
||||||
param = sig.parameters[name]
|
|
||||||
ann = param.annotation
|
|
||||||
actual_t = type(value)
|
|
||||||
|
|
||||||
if exact_match(ann, actual_t):
|
|
||||||
score += 30
|
|
||||||
elif ann is inspect._empty or is_any(ann) or get_origin(ann) is not None and get_origin(ann) is t.Union and any(is_any(a) for a in get_args(ann)):
|
|
||||||
score += 0
|
|
||||||
elif type_matches(ann, actual_t):
|
|
||||||
dist = subclass_depth(ann, actual_t)
|
|
||||||
score += max(15 - min(dist, 10), 5)
|
|
||||||
else:
|
|
||||||
if allow_coercion and isinstance(ann, type):
|
|
||||||
can, coerced = Coercions.can_coerce(ann, value)
|
|
||||||
if can:
|
|
||||||
coercions_to_apply[name] = coerced
|
|
||||||
score += 8
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
origin = get_origin(ann)
|
|
||||||
if allow_coercion and origin is t.Union:
|
|
||||||
ok = False
|
|
||||||
for opt in get_args(ann):
|
|
||||||
if opt is type(None):
|
|
||||||
continue
|
|
||||||
if isinstance(opt, type):
|
|
||||||
can, coerced = Coercions.can_coerce(opt, value)
|
|
||||||
if can:
|
|
||||||
coercions_to_apply[name] = coerced
|
|
||||||
score += 6
|
|
||||||
ok = True
|
|
||||||
break
|
|
||||||
if not ok:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if expect_type is not None:
|
|
||||||
ret_ann = sig.return_annotation
|
|
||||||
if ret_ann is inspect._empty or is_any(ret_ann):
|
|
||||||
score -= 1
|
|
||||||
else:
|
|
||||||
if get_origin(ret_ann) is t.Union:
|
|
||||||
ok = any(type_matches(opt, expect_type) for opt in get_args(ret_ann))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
ok = issubclass(expect_type, ret_ann) or issubclass(ret_ann, expect_type)
|
|
||||||
except TypeError:
|
|
||||||
ok = True
|
|
||||||
if not ok:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
score += 5
|
|
||||||
|
|
||||||
score += prio * 1000
|
|
||||||
return score, coercions_to_apply
|
|
||||||
|
|
||||||
def _dispatch(self, instance, owner, *args, **kwargs):
|
|
||||||
expect_type = kwargs.pop("__expect__", None)
|
|
||||||
|
|
||||||
key = (tuple(type(a) for a in args), tuple(sorted(kwargs.keys())), expect_type)
|
|
||||||
cached = self._cache.get(key)
|
|
||||||
if cached:
|
|
||||||
entry = cached
|
|
||||||
sig = entry["sig"]
|
|
||||||
bound = sig.bind(instance, *args, **kwargs)
|
|
||||||
bound.apply_defaults()
|
|
||||||
return entry["func"](*bound.args, **bound.kwargs)
|
|
||||||
|
|
||||||
candidates = []
|
|
||||||
for entry in self.overloads:
|
|
||||||
scored = self._score_entry(entry, instance, args, kwargs, expect_type)
|
|
||||||
if scored is not None:
|
|
||||||
score, coercions = scored
|
|
||||||
candidates.append((score, random.random(), coercions, entry))
|
|
||||||
|
|
||||||
if not candidates:
|
|
||||||
raise TypeError(f"No matching overload for {self.name}{args}")
|
|
||||||
|
|
||||||
candidates.sort(key=lambda x: (x[0], x[1]), reverse=True)
|
|
||||||
best_score, _, coercions, entry = candidates[0]
|
|
||||||
|
|
||||||
sig = entry["sig"]
|
|
||||||
bound = sig.bind(instance, *args, **kwargs)
|
|
||||||
bound.apply_defaults()
|
|
||||||
for k, v in coercions.items():
|
|
||||||
bound.arguments[k] = v
|
|
||||||
|
|
||||||
self._cache.put(key, entry)
|
|
||||||
return entry["func"](*bound.args, **bound.kwargs)
|
|
||||||
|
|
||||||
def overload(dispatcher: Dispatcher, *, priority: int = 0):
|
|
||||||
def decorator(func):
|
|
||||||
dispatcher.register(func, priority=priority)
|
|
||||||
return dispatcher
|
|
||||||
return decorator
|
|
||||||
54
multinut/stultus/__init__.py
Normal file
54
multinut/stultus/__init__.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from .custom import (
|
||||||
|
EQUALS, GREATER, LESSER, GREATER_EQUAL,
|
||||||
|
LESSER_EQUAL, NOT, AND, OR
|
||||||
|
)
|
||||||
|
|
||||||
|
from .logic import (
|
||||||
|
StultusMixin, StultusInt, StultusFloat,
|
||||||
|
StultusStr, StultusBool, StultusList, StultusDict
|
||||||
|
)
|
||||||
|
|
||||||
|
from .handler import ping, StultusUnavailable
|
||||||
|
|
||||||
|
from .math_ops import ADD, SUB, MUL, DIV, RANDINT, RANGE
|
||||||
|
from .lists import LEN, LIST_INDEX, LIST_PUSH, LIST_POP
|
||||||
|
from .dicts import DICT_GET
|
||||||
|
from .strings import UPPER, INVERT
|
||||||
|
from .truthy import TRUTH, BOOLIFY
|
||||||
|
from .crypto import HASH_PASSWORD
|
||||||
|
from .time_utils import TIME
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# logic
|
||||||
|
"EQUALS", "GREATER", "LESSER",
|
||||||
|
"GREATER_EQUAL", "LESSER_EQUAL", "NOT",
|
||||||
|
"AND", "OR",
|
||||||
|
|
||||||
|
# types
|
||||||
|
"StultusMixin", "StultusInt", "StultusFloat",
|
||||||
|
"StultusStr", "StultusBool", "StultusList", "StultusDict",
|
||||||
|
|
||||||
|
# base
|
||||||
|
"ping", "StultusUnavailable",
|
||||||
|
|
||||||
|
# math
|
||||||
|
"ADD", "SUB", "MUL", "DIV", "RANDINT", "RANGE",
|
||||||
|
|
||||||
|
# lists
|
||||||
|
"LEN", "LIST_INDEX", "LIST_PUSH", "LIST_POP",
|
||||||
|
|
||||||
|
# dicts
|
||||||
|
"DICT_GET",
|
||||||
|
|
||||||
|
# strings
|
||||||
|
"UPPER", "INVERT",
|
||||||
|
|
||||||
|
# truthiness
|
||||||
|
"TRUTH", "BOOLIFY",
|
||||||
|
|
||||||
|
# crypto
|
||||||
|
"HASH_PASSWORD",
|
||||||
|
|
||||||
|
# time
|
||||||
|
"TIME"
|
||||||
|
]
|
||||||
5
multinut/stultus/crypto.py
Normal file
5
multinut/stultus/crypto.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from .handler import _post
|
||||||
|
|
||||||
|
def HASH_PASSWORD(password: str, algo: str = "sha256"):
|
||||||
|
result = _post("hash_password", {"password": password, "algo": algo})
|
||||||
|
return result.get("hash")
|
||||||
54
multinut/stultus/custom.py
Normal file
54
multinut/stultus/custom.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
from .logic import (
|
||||||
|
EQUALS, GREATER, LESSER, GREATER_EQUAL,
|
||||||
|
LESSER_EQUAL, NOT, AND, OR
|
||||||
|
)
|
||||||
|
from .math_ops import ADD, SUB, MUL, DIV
|
||||||
|
from .strings import UPPER, INVERT
|
||||||
|
from .truthy import TRUTH
|
||||||
|
from .lists import LIST_INDEX, LIST_PUSH, LIST_POP
|
||||||
|
from .dicts import DICT_GET
|
||||||
|
|
||||||
|
# Core mixin for logic + math
|
||||||
|
class StultusMixin:
|
||||||
|
def __eq__(self, other): return EQUALS(self, other)
|
||||||
|
def __ne__(self, other): return NOT(EQUALS(self, other))
|
||||||
|
def __gt__(self, other): return GREATER(self, other)
|
||||||
|
def __lt__(self, other): return LESSER(self, other)
|
||||||
|
def __ge__(self, other): return GREATER_EQUAL(self, other)
|
||||||
|
def __le__(self, other): return LESSER_EQUAL(self, other)
|
||||||
|
def __and__(self, other): return AND(self, other)
|
||||||
|
def __or__(self, other): return OR(self, other)
|
||||||
|
def __bool__(self): return TRUTH(self)
|
||||||
|
|
||||||
|
# Math extensions
|
||||||
|
class StultusInt(StultusMixin, int):
|
||||||
|
def __add__(self, other): return ADD(self, other)
|
||||||
|
def __sub__(self, other): return SUB(self, other)
|
||||||
|
def __mul__(self, other): return MUL(self, other)
|
||||||
|
def __truediv__(self, other): return DIV(self, other)
|
||||||
|
|
||||||
|
class StultusFloat(StultusMixin, float):
|
||||||
|
def __add__(self, other): return ADD(self, other)
|
||||||
|
def __sub__(self, other): return SUB(self, other)
|
||||||
|
def __mul__(self, other): return MUL(self, other)
|
||||||
|
def __truediv__(self, other): return DIV(self, other)
|
||||||
|
|
||||||
|
# String extensions
|
||||||
|
class StultusStr(StultusMixin, str):
|
||||||
|
def upper(self): return UPPER(self)
|
||||||
|
def invert(self): return INVERT(self)
|
||||||
|
def __getitem__(self, index): return LIST_INDEX(list(self), index)
|
||||||
|
|
||||||
|
# Bool wrapper
|
||||||
|
class StultusBool(StultusMixin, bool):
|
||||||
|
pass # already supports logic
|
||||||
|
|
||||||
|
# List wrapper
|
||||||
|
class StultusList(StultusMixin, list):
|
||||||
|
def push(self, item): return LIST_PUSH(self.copy(), item)
|
||||||
|
def pop(self): return LIST_POP(self.copy())
|
||||||
|
def __getitem__(self, index): return LIST_INDEX(self, index)
|
||||||
|
|
||||||
|
# Dict wrapper
|
||||||
|
class StultusDict(StultusMixin, dict):
|
||||||
|
def __getitem__(self, key): return DICT_GET(self, key)
|
||||||
3
multinut/stultus/dicts.py
Normal file
3
multinut/stultus/dicts.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from .handler import _post
|
||||||
|
|
||||||
|
def DICT_GET(dct, key): return _post("dict_get", {"dict": dct, "key": key})
|
||||||
30
multinut/stultus/handler.py
Normal file
30
multinut/stultus/handler.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
BASE_URL = "https://stultus.chipperfluff.at"
|
||||||
|
|
||||||
|
class StultusUnavailable(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get(path: str, params: dict | list = None) -> any:
|
||||||
|
try:
|
||||||
|
r = requests.get(f"{BASE_URL}/{path}", params=params, timeout=5)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json().get("result")
|
||||||
|
except Exception as e:
|
||||||
|
raise StultusUnavailable(f"[STULTUS] Failed GET {path}: {e}")
|
||||||
|
|
||||||
|
def _post(path: str, json: dict = None) -> any:
|
||||||
|
try:
|
||||||
|
r = requests.post(f"{BASE_URL}/{path}", json=json, timeout=5)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json().get("result") or r.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise StultusUnavailable(f"[STULTUS] Failed POST {path}: {e}")
|
||||||
|
|
||||||
|
def ping() -> bool:
|
||||||
|
try:
|
||||||
|
r = requests.get(f"{BASE_URL}/ping", timeout=2)
|
||||||
|
return r.status_code == 200 and r.json().get("status") == "ok"
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
6
multinut/stultus/lists.py
Normal file
6
multinut/stultus/lists.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from .handler import _post
|
||||||
|
|
||||||
|
def LEN(value): return _post("len", {"value": value}).get("length")
|
||||||
|
def LIST_INDEX(lst, index): return _post("list_index", {"list": lst, "index": index})
|
||||||
|
def LIST_PUSH(lst, item): return _post("list_push", {"list": lst, "item": item})
|
||||||
|
def LIST_POP(lst): return _post("list_pop", {"list": lst})
|
||||||
11
multinut/stultus/logic.py
Normal file
11
multinut/stultus/logic.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from .handler import _get
|
||||||
|
|
||||||
|
def EQUALS(a, b): return _get("equals", {"a": a, "b": b})
|
||||||
|
def GREATER(a, b): return _get("greater", {"a": a, "b": b})
|
||||||
|
def LESSER(a, b): return _get("lesser", {"a": a, "b": b})
|
||||||
|
def NOT(x): return _get("not", {"val": str(x).lower()})
|
||||||
|
def AND(*args): return _get("and", [("val", str(v).lower()) for v in args])
|
||||||
|
def OR(*args): return _get("or", [("val", str(v).lower()) for v in args])
|
||||||
|
|
||||||
|
def GREATER_EQUAL(a, b): return OR(GREATER(a, b), EQUALS(a, b))
|
||||||
|
def LESSER_EQUAL(a, b): return OR(LESSER(a, b), EQUALS(a, b))
|
||||||
8
multinut/stultus/math_ops.py
Normal file
8
multinut/stultus/math_ops.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from .handler import _get
|
||||||
|
|
||||||
|
def ADD(a, b): return _get("add", {"a": a, "b": b})
|
||||||
|
def SUB(a, b): return _get("sub", {"a": a, "b": b})
|
||||||
|
def MUL(a, b): return _get("mul", {"a": a, "b": b})
|
||||||
|
def DIV(a, b): return _get("div", {"a": a, "b": b})
|
||||||
|
def RANDINT(a, b): return _get("randint", {"a": a, "b": b})
|
||||||
|
def RANGE(n, shuffle=False): return _get("range", {"n": n, "shuffle": str(shuffle).lower()})
|
||||||
4
multinut/stultus/strings.py
Normal file
4
multinut/stultus/strings.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .handler import _get
|
||||||
|
|
||||||
|
def UPPER(value: str): return _get("upper", {"value": value})
|
||||||
|
def INVERT(value: str): return _get("invert_string", {"value": value})
|
||||||
5
multinut/stultus/time_utils.py
Normal file
5
multinut/stultus/time_utils.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from .handler import _get
|
||||||
|
|
||||||
|
def TIME():
|
||||||
|
data = _get("time")
|
||||||
|
return data # epoch, iso, certainty
|
||||||
4
multinut/stultus/truthy.py
Normal file
4
multinut/stultus/truthy.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .handler import _post, _get
|
||||||
|
|
||||||
|
def TRUTH(value): return _post("truth", {"value": value}).get("truth")
|
||||||
|
def BOOLIFY(value: str): return _get("boolify", {"value": value})
|
||||||
5
setup.py
5
setup.py
@ -2,12 +2,11 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='multinut',
|
name='multinut',
|
||||||
version='0.3.1',
|
version='0.2.9',
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"requests>=2.25.0",
|
"requests>=2.25.0",
|
||||||
"python-dotenv>=0.21.0",
|
"python-dotenv>=0.21.0"
|
||||||
"openai>=0.26.5"
|
|
||||||
],
|
],
|
||||||
author='Chipperfluff',
|
author='Chipperfluff',
|
||||||
author_email='contact@chipperfluff.at',
|
author_email='contact@chipperfluff.at',
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
from multinut.explain import Explainable, hello
|
|
||||||
|
|
||||||
class MyClass(Explainable):
|
|
||||||
def my_method(self, x):
|
|
||||||
"""This method does something."""
|
|
||||||
return x * 2
|
|
||||||
|
|
||||||
def another(self, msg: str):
|
|
||||||
return msg[::-1]
|
|
||||||
|
|
||||||
|
|
||||||
print("=== Class Explanation ===")
|
|
||||||
print(MyClass.explain())
|
|
||||||
|
|
||||||
print("\n=== Method Explanation: my_method ===")
|
|
||||||
print(MyClass.my_method.explain())
|
|
||||||
|
|
||||||
print("\n=== Method Explanation: another ===")
|
|
||||||
print(MyClass.another.explain())
|
|
||||||
Loading…
x
Reference in New Issue
Block a user