isik 0.1.0__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.
Files changed (49) hide show
  1. isik/__init__.py +1 -0
  2. isik/_internal/__init__.py +11 -0
  3. isik/common/__init__.py +0 -0
  4. isik/common/config/__init__.py +25 -0
  5. isik/common/config/builder.py +105 -0
  6. isik/common/config/casters.py +56 -0
  7. isik/common/config/exceptions.py +1 -0
  8. isik/common/utils/__init__.py +46 -0
  9. isik/common/utils/caching.py +21 -0
  10. isik/common/utils/concurrency.py +83 -0
  11. isik/common/utils/error_handling.py +155 -0
  12. isik/common/utils/functional.py +190 -0
  13. isik/common/utils/iterables.py +41 -0
  14. isik/common/utils/metaclasses.py +70 -0
  15. isik/common/utils/sentinel.py +38 -0
  16. isik/common/utils/strings.py +23 -0
  17. isik/django/__init__.py +3 -0
  18. isik/django/apps/__init__.py +0 -0
  19. isik/django/apps/common/__init__.py +0 -0
  20. isik/django/apps/common/admin.py +119 -0
  21. isik/django/apps/common/apps.py +15 -0
  22. isik/django/apps/common/backends/__init__.py +0 -0
  23. isik/django/apps/common/backends/auth.py +20 -0
  24. isik/django/apps/common/db.py +13 -0
  25. isik/django/apps/common/email.py +10 -0
  26. isik/django/apps/common/fields/__init__.py +0 -0
  27. isik/django/apps/common/fields/gfk.py +101 -0
  28. isik/django/apps/common/lookups.py +6 -0
  29. isik/django/apps/common/middleware/__init__.py +0 -0
  30. isik/django/apps/common/middleware/media_white_noise.py +42 -0
  31. isik/django/apps/common/middleware/session.py +26 -0
  32. isik/django/apps/common/models.py +89 -0
  33. isik/django/apps/common/orm.py +22 -0
  34. isik/django/apps/common/skippable_validators.py +72 -0
  35. isik/django/drf/__init__.py +3 -0
  36. isik/django/drf/error_handling.py +17 -0
  37. isik/django/drf/filters.py +12 -0
  38. isik/django/drf/pagination.py +40 -0
  39. isik/django/drf/permissions.py +108 -0
  40. isik/django/http_exceptions/__init__.py +0 -0
  41. isik/django/http_exceptions/decorators.py +29 -0
  42. isik/django/http_exceptions/exceptions.py +107 -0
  43. isik/django/http_exceptions/middleware.py +55 -0
  44. isik/sentry/__init__.py +3 -0
  45. isik/sentry/utils/__init__.py +9 -0
  46. isik-0.1.0.dist-info/METADATA +40 -0
  47. isik-0.1.0.dist-info/RECORD +49 -0
  48. isik-0.1.0.dist-info/WHEEL +4 -0
  49. isik-0.1.0.dist-info/licenses/LICENCE +21 -0
isik/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,11 @@
1
+ import importlib
2
+
3
+
4
+ def check_extra(extra_name, package_name):
5
+ try:
6
+ importlib.import_module(package_name)
7
+ except ImportError as exception:
8
+ raise ImportError(
9
+ f"The module you are trying to use requires '{extra_name}'. "
10
+ f"Please install it with: pip install isik[{extra_name}]"
11
+ ) from exception
File without changes
@@ -0,0 +1,25 @@
1
+ from isik.common.config.builder import Config, config
2
+ from isik.common.config.casters import (
3
+ boolean,
4
+ caster,
5
+ comma_separated_float_list,
6
+ comma_separated_int_list,
7
+ comma_separated_list,
8
+ integer,
9
+ string,
10
+ )
11
+ from isik.common.config.exceptions import ConfigError
12
+
13
+
14
+ __all__ = [
15
+ "Config",
16
+ "ConfigError",
17
+ "boolean",
18
+ "caster",
19
+ "comma_separated_float_list",
20
+ "comma_separated_int_list",
21
+ "comma_separated_list",
22
+ "config",
23
+ "integer",
24
+ "string",
25
+ ]
@@ -0,0 +1,105 @@
1
+ import os
2
+
3
+ from isik.common.config.exceptions import ConfigError
4
+
5
+
6
+ _MISSING = object()
7
+
8
+
9
+ class Config(dict):
10
+ """
11
+ A dict that also allows attribute access, e.g. config.DATABASE.HOST, and can reload its
12
+ values from the environment via refresh() - callable on the root config or on any nested
13
+ one, refreshing just that subtree in place so existing references to it see the update too.
14
+
15
+ A schema key that collides with a dict method name (items, keys, ...) or with refresh
16
+ itself is only reachable via item access (config["refresh"]), not attribute access.
17
+ """
18
+
19
+ __setattr__ = dict.__setitem__
20
+
21
+ def __getattr__(self, name):
22
+ try:
23
+ return self[name]
24
+ except KeyError as exc:
25
+ raise AttributeError(name) from exc
26
+
27
+ def refresh(self, *path):
28
+ """
29
+ Re-read values from the environment, in place. With no arguments, refreshes every
30
+ value under this node. With a path of key names, refreshes only that one nested
31
+ value instead - e.g. config.refresh("DATABASE", "HOST") or, equivalently,
32
+ config.DATABASE.refresh("HOST").
33
+ """
34
+ if not path:
35
+ for key in self._schema:
36
+ self._refresh_key(key)
37
+ return self
38
+
39
+ key, *rest = path
40
+ if rest:
41
+ self[key].refresh(*rest)
42
+ else:
43
+ self._refresh_key(key)
44
+ return self
45
+
46
+ def _refresh_key(self, key):
47
+ try:
48
+ value = self._schema[key]
49
+ except KeyError:
50
+ raise ConfigError(f"'{key}' is not a key in this config.") from None
51
+ if isinstance(value, dict):
52
+ self[key].refresh()
53
+ else:
54
+ self[key] = _read_leaf(value, _environment_key(self._prefix, [*self._path, key], self._sep))
55
+
56
+
57
+ def _environment_key(prefix, path, sep):
58
+ return sep.join([*([prefix] if prefix else []), *path])
59
+
60
+
61
+ def _read_leaf(caster, environment_key):
62
+ try:
63
+ raw_value = os.environ[environment_key]
64
+ except KeyError:
65
+ default = getattr(caster, "missing_default", _MISSING)
66
+ if default is _MISSING:
67
+ raise ConfigError(
68
+ f"Environment variable {environment_key} not found."
69
+ f" Please set it or provide a `missing_default` to your caster."
70
+ ) from None
71
+ return default
72
+
73
+ try:
74
+ return caster(raw_value)
75
+ except Exception as exception:
76
+ default = getattr(caster, "error_default", _MISSING)
77
+ if default is _MISSING:
78
+ raise ConfigError(
79
+ f"Error while parsing {environment_key}={raw_value!r} with '{caster}'."
80
+ " Please check the value and the caster or provide an `error_default` to your caster."
81
+ ) from exception
82
+ return default
83
+
84
+
85
+ def _build(data, path, prefix, sep):
86
+ result = Config()
87
+ object.__setattr__(result, "_schema", data)
88
+ object.__setattr__(result, "_path", path)
89
+ object.__setattr__(result, "_prefix", prefix)
90
+ object.__setattr__(result, "_sep", sep)
91
+ for key, value in data.items():
92
+ key_path = [*path, key]
93
+ if isinstance(value, dict):
94
+ result[key] = _build(value, key_path, prefix, sep)
95
+ elif callable(value):
96
+ result[key] = _read_leaf(value, _environment_key(prefix, key_path, sep))
97
+ else:
98
+ raise ConfigError(
99
+ f"Values either must be callables or other mappings, not {type(value)}. Key={'.'.join(key_path)}."
100
+ )
101
+ return result
102
+
103
+
104
+ def config(data, *, prefix=None, sep="__"):
105
+ return _build(data, [], prefix, sep)
@@ -0,0 +1,56 @@
1
+ from functools import wraps
2
+
3
+
4
+ def caster(f):
5
+ """
6
+ Turns a plain string->value function into a config caster that accepts an optional
7
+ missing_default (used when the environment variable isn't set) and error_default (used
8
+ when f(value) raises). Neither is set by default, meaning a missing or unparseable
9
+ value raises ConfigError instead.
10
+ """
11
+ sentinel = object()
12
+
13
+ def wrapper(missing_default=sentinel, error_default=sentinel):
14
+ @wraps(f)
15
+ def function_clone(value):
16
+ return f(value)
17
+
18
+ if missing_default is not sentinel:
19
+ function_clone.missing_default = missing_default
20
+ if error_default is not sentinel:
21
+ function_clone.error_default = error_default
22
+ return function_clone
23
+
24
+ return wrapper
25
+
26
+
27
+ @caster
28
+ def comma_separated_list(value):
29
+ return value.split(",")
30
+
31
+
32
+ @caster
33
+ def comma_separated_int_list(value):
34
+ return [int(i) for i in value.split(",")]
35
+
36
+
37
+ @caster
38
+ def comma_separated_float_list(value):
39
+ return [float(i) for i in value.split(",")]
40
+
41
+
42
+ @caster
43
+ def boolean(value):
44
+ truthy = ["true", "True", "1"]
45
+ falsy = ["false", "False", "0"]
46
+
47
+ if value in truthy:
48
+ return True
49
+ elif value in falsy:
50
+ return False
51
+ else:
52
+ raise ValueError(f"Value {value!r} can not be parsed into a boolean.")
53
+
54
+
55
+ string = caster(str)
56
+ integer = caster(int)
@@ -0,0 +1 @@
1
+ class ConfigError(ValueError): ...
@@ -0,0 +1,46 @@
1
+ from isik.common.utils.caching import get_cached
2
+ from isik.common.utils.concurrency import ContextLocal, ThreadLocal, ThreadLock
3
+ from isik.common.utils.error_handling import SuppressAndRun, TransformExceptions, suppress_callable
4
+ from isik.common.utils.functional import (
5
+ cloned,
6
+ enabled_if,
7
+ identity,
8
+ noop,
9
+ raises,
10
+ require_exclusive_keys,
11
+ returns,
12
+ with_attrs,
13
+ )
14
+ from isik.common.utils.iterables import all_combinations, first_of, not_none, purge_iterable, purge_mapping
15
+ from isik.common.utils.metaclasses import transform
16
+ from isik.common.utils.sentinel import Sentinel
17
+ from isik.common.utils.strings import camel_to_snake, snake_to_human, snake_to_pascal
18
+
19
+
20
+ __all__ = [
21
+ "ContextLocal",
22
+ "Sentinel",
23
+ "SuppressAndRun",
24
+ "ThreadLocal",
25
+ "ThreadLock",
26
+ "TransformExceptions",
27
+ "all_combinations",
28
+ "camel_to_snake",
29
+ "cloned",
30
+ "enabled_if",
31
+ "first_of",
32
+ "get_cached",
33
+ "identity",
34
+ "noop",
35
+ "not_none",
36
+ "purge_iterable",
37
+ "purge_mapping",
38
+ "raises",
39
+ "require_exclusive_keys",
40
+ "returns",
41
+ "snake_to_human",
42
+ "snake_to_pascal",
43
+ "suppress_callable",
44
+ "transform",
45
+ "with_attrs",
46
+ ]
@@ -0,0 +1,21 @@
1
+ def get_cached(obj, attr, factory):
2
+ """
3
+ Get a cached attribute from an object, computing and storing it if absent.
4
+
5
+ Bypasses __getattribute__ via object.__getattribute__ and object.__setattr__,
6
+ making it safe to call from within a custom __getattribute__ implementation.
7
+
8
+ Args:
9
+ obj: The object to cache the attribute on.
10
+ attr: The attribute name to use as the cache key.
11
+ factory: A zero-argument callable that computes the value on cache miss.
12
+
13
+ Returns:
14
+ The cached value if present, otherwise the result of calling factory().
15
+ """
16
+ try:
17
+ return object.__getattribute__(obj, attr)
18
+ except AttributeError:
19
+ value = factory()
20
+ object.__setattr__(obj, attr, value)
21
+ return value
@@ -0,0 +1,83 @@
1
+ import threading
2
+ from contextvars import ContextVar
3
+
4
+
5
+ class ThreadLocal:
6
+ """
7
+ A factory for thread local identified by name.
8
+ Calling ThreadLocal("FOO") returns a thread local object shared across all calls with the same name.
9
+ """
10
+
11
+ _registry = {}
12
+ _registry_lock = threading.Lock()
13
+
14
+ def __new__(cls, name):
15
+ if name not in cls._registry:
16
+ with cls._registry_lock:
17
+ if name not in cls._registry:
18
+ cls._registry[name] = threading.local()
19
+ return cls._registry[name]
20
+
21
+
22
+ class ThreadLock:
23
+ """
24
+ A factory for thread lock identified by name.
25
+ Calling ThreadLock("FOO") returns a lock object shared across all calls with the same name.
26
+ """
27
+
28
+ _registry = {}
29
+ _registry_lock = threading.Lock()
30
+
31
+ def __new__(cls, name):
32
+ if name not in cls._registry:
33
+ with cls._registry_lock:
34
+ if name not in cls._registry:
35
+ cls._registry[name] = threading.Lock()
36
+ return cls._registry[name]
37
+
38
+
39
+ class ContextLocal:
40
+ """
41
+ A named, registry-backed namespace of ContextVars.
42
+ Calling ContextLocal("FOO") returns the same instance across all calls with the same name,
43
+ making it safe for async and coroutine contexts.
44
+
45
+ Usage:
46
+ local = ContextLocal("MY_NAMESPACE")
47
+ token = local.set("foo", "bar")
48
+ local.get("foo") # "bar"
49
+ local.get("foo", "default") # "bar"
50
+ local.reset("foo", token)
51
+ """
52
+
53
+ _registry = {}
54
+ _registry_lock = threading.Lock()
55
+
56
+ def __new__(cls, name):
57
+ if name not in cls._registry:
58
+ with cls._registry_lock:
59
+ if name not in cls._registry:
60
+ instance = super().__new__(cls)
61
+ object.__setattr__(instance, "_name", name)
62
+ object.__setattr__(instance, "_vars", {})
63
+ cls._registry[name] = instance
64
+ return cls._registry[name]
65
+
66
+ def _get_var(self, key):
67
+ vars_ = object.__getattribute__(self, "_vars")
68
+ if key not in vars_:
69
+ name = object.__getattribute__(self, "_name")
70
+ vars_[key] = ContextVar(f"{name}.{key}")
71
+ return vars_[key]
72
+
73
+ def get(self, key, *args):
74
+ """Get a value. Accepts an optional default as a second argument, like dict.get()."""
75
+ return self._get_var(key).get(*args)
76
+
77
+ def set(self, key, value):
78
+ """Set a value and return a token that can be used to restore the previous state."""
79
+ return self._get_var(key).set(value)
80
+
81
+ def reset(self, key, token):
82
+ """Reset a value to its state before the corresponding set() call."""
83
+ self._get_var(key).reset(token)
@@ -0,0 +1,155 @@
1
+ from contextlib import ContextDecorator, suppress
2
+ from functools import wraps
3
+
4
+ from isik.common.utils.functional import require_exclusive_keys
5
+
6
+
7
+ class TransformExceptions(ContextDecorator):
8
+ """
9
+ A context manager and decorator that catches the given exception types and transforms
10
+ them into a new exception via a transformation function.
11
+
12
+ Args:
13
+ *exception_types: One or more exception types to catch and transform.
14
+ transform: A callable that receives the original exception and returns a new one.
15
+ Can be left out and filled in later - see the two-step form below.
16
+ keep_original: If True (default), the original exception is chained to the new one
17
+ via `raise new from original`, preserving the traceback context.
18
+ If False, the original exception is suppressed and the new one is
19
+ raised in isolation.
20
+
21
+ Raises:
22
+ Whatever exception `transform` returns: at call time if a matching exception is caught.
23
+
24
+ Example:
25
+ @TransformExceptions(ValueError, transform=lambda e: MyCustomError(str(e)))
26
+ def parse(x):
27
+ ...
28
+
29
+ with TransformExceptions(ValueError, KeyError, transform=lambda e: MyCustomError(str(e))):
30
+ ...
31
+
32
+ # Without chaining:
33
+ with TransformExceptions(ValueError, transform=lambda e: MyCustomError(str(e)), keep_original=False):
34
+ ...
35
+
36
+ # Two-step form: give it exception types now, decorate the transform function
37
+ # later - useful when the transform itself is more than a lambda one-liner.
38
+ # The transform function becomes a named, reusable decorator.
39
+ @TransformExceptions(ValueError)
40
+ def value_error_to_my_error(e):
41
+ return MyCustomError(str(e))
42
+
43
+ @value_error_to_my_error
44
+ def parse(x):
45
+ ...
46
+ """
47
+
48
+ def __init__(self, *exception_types, transform=None, keep_original=True):
49
+ self.exception_types = exception_types
50
+ self.transform = transform
51
+ self.keep_original = keep_original
52
+
53
+ def __call__(self, func):
54
+ if self.transform is None:
55
+ # two-step form: `func` is the transform, not the guarded code
56
+ self.transform = func
57
+ return self
58
+ return super().__call__(func)
59
+
60
+ def __enter__(self):
61
+ return self
62
+
63
+ def __exit__(self, exc_type, exc_val, exc_tb):
64
+ if isinstance(exc_val, self.exception_types):
65
+ if self.transform is None:
66
+ raise TypeError(
67
+ "TransformExceptions has no transform set - "
68
+ "pass transform=... or decorate a transform function with it first."
69
+ )
70
+ new_exception = self.transform(exc_val)
71
+ raise new_exception from (exc_val if self.keep_original else None)
72
+ return False
73
+
74
+
75
+ class SuppressAndRun(suppress):
76
+ """
77
+ A context manager that suppresses the given exceptions and calls a function with the
78
+ suppressed exception.
79
+
80
+ Extends contextlib.suppress with the ability to run a callable when an exception is
81
+ suppressed, for example to log it, print it, or send it to an error tracker.
82
+
83
+ Args:
84
+ *exceptions: One or more exception types to suppress.
85
+ func: A callable that receives the suppressed exception as its only argument.
86
+ Defaults to print. Must accept a single exception argument.
87
+
88
+ Example:
89
+ with SuppressAndRun(ValueError, func=logger.warning):
90
+ raise ValueError("oops") # suppressed, logger.warning called with the exception
91
+
92
+ with SuppressAndRun(ValueError, KeyError, func=my_handler):
93
+ ...
94
+ """
95
+
96
+ def __init__(self, *exceptions, func=print):
97
+ super().__init__(*exceptions)
98
+ self.func = func
99
+
100
+ def __exit__(self, exc_type, exc_val, exc_tb):
101
+ suppressed = super().__exit__(exc_type, exc_val, exc_tb)
102
+ if suppressed:
103
+ self.func(exc_val)
104
+ return suppressed
105
+
106
+
107
+ @require_exclusive_keys(
108
+ {"by_return_value": ["return_value"]},
109
+ {"by_return_func": ["return_func"]},
110
+ allow_empty=True,
111
+ )
112
+ def suppress_callable(*exceptions, func=print, return_value=None, return_func=None):
113
+ """
114
+ A decorator that suppresses the given exceptions, calls func with the suppressed
115
+ exception, and returns either a static value or the result of a replacement function.
116
+
117
+ Args:
118
+ *exceptions: One or more exception types to suppress.
119
+ func: A callable that receives the suppressed exception as its only argument.
120
+ Defaults to print.
121
+ return_value: A static value to return when an exception is suppressed.
122
+ Mutually exclusive with return_func.
123
+ return_func: A callable with the same signature as the decorated function.
124
+ Called with the original arguments when an exception is suppressed.
125
+ Mutually exclusive with return_value.
126
+
127
+ Raises:
128
+ ValueError: If both return_value and return_func are provided.
129
+
130
+ Example:
131
+ @suppress_callable(ValueError, func=logger.warning, return_value=0)
132
+ def parse(x):
133
+ ...
134
+
135
+ @suppress_callable(ValueError, return_func=lambda x: x * 0)
136
+ def parse(x):
137
+ ...
138
+
139
+ @suppress_callable(ValueError)
140
+ def parse(x):
141
+ ... # returns None when suppressed
142
+ """
143
+
144
+ def decorator(f):
145
+ @wraps(f)
146
+ def wrapper(*a, **kw):
147
+ with SuppressAndRun(*exceptions, func=func):
148
+ return f(*a, **kw)
149
+ if return_func is not None:
150
+ return return_func(*a, **kw)
151
+ return return_value
152
+
153
+ return wrapper
154
+
155
+ return decorator