stackure 1.0.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.
stackure/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """Stackure Python SDK for authentication and session management.
2
+
3
+ Four public entry points:
4
+
5
+ * :func:`auth` — decorator that enforces authentication on a view
6
+ * :func:`verify` — non-throwing check that returns a :class:`VerifyResult`
7
+ * :func:`send_magic_link` — trigger a magic-link sign-in email
8
+ * :func:`logout` — revoke a session
9
+
10
+ Plus the supporting types :class:`User`, :class:`VerifyResult`,
11
+ :class:`MagicLinkResponse`, and the single error type :class:`StackureError`.
12
+
13
+ Example:
14
+ >>> import stackure
15
+ >>> await stackure.send_magic_link(email="user@example.com", app_id="...")
16
+ """
17
+
18
+ from .client import logout, send_magic_link
19
+ from .errors import StackureError
20
+ from .middleware import auth, verify
21
+ from .types import MagicLinkResponse, User, VerifyResult
22
+
23
+ __all__ = [
24
+ "auth",
25
+ "verify",
26
+ "send_magic_link",
27
+ "logout",
28
+ "User",
29
+ "VerifyResult",
30
+ "MagicLinkResponse",
31
+ "StackureError",
32
+ ]
stackure/client.py ADDED
@@ -0,0 +1,150 @@
1
+ """Internal HTTP layer for the Stackure SDK.
2
+
3
+ Consumers should use the module-level ``send_magic_link`` and ``logout``
4
+ functions re-exported from ``stackure``; they go through this layer.
5
+ """
6
+
7
+ import asyncio
8
+ import os
9
+
10
+ import httpx
11
+
12
+ from .errors import StackureError
13
+ from .types import MagicLinkResponse, User
14
+ from .validation import validate_email, validate_uuid
15
+
16
+ _DEFAULT_BASE_URL = "https://stackure.com"
17
+ _REQUEST_TIMEOUT_S = 10.0
18
+ _MAX_RETRIES = 2
19
+
20
+
21
+ def _base_url() -> str:
22
+ """Resolve the base URL from ``STACKURE_BASE_URL`` or fall back to production."""
23
+ env = os.environ.get("STACKURE_BASE_URL")
24
+ return env.rstrip("/") if env else _DEFAULT_BASE_URL
25
+
26
+
27
+ async def _request(method: str, path: str, **kwargs) -> httpx.Response:
28
+ """Perform an HTTP request with retry + timeout.
29
+
30
+ Retries 5xx responses twice with exponential backoff (500ms, 1s). Timeouts
31
+ are never retried — a second attempt would obscure real latency.
32
+ """
33
+ url = f"{_base_url()}{path}"
34
+ last_error: Exception | None = None
35
+
36
+ for attempt in range(_MAX_RETRIES + 1):
37
+ if attempt > 0:
38
+ await asyncio.sleep(0.5 * (2 ** (attempt - 1)))
39
+ try:
40
+ async with httpx.AsyncClient(timeout=_REQUEST_TIMEOUT_S) as http_client:
41
+ response = await http_client.request(method, url, **kwargs)
42
+ if response.status_code >= 500 and attempt < _MAX_RETRIES:
43
+ last_error = StackureError(
44
+ "network",
45
+ f"Server error ({response.status_code})",
46
+ response.status_code,
47
+ )
48
+ continue
49
+ return response
50
+ except httpx.TimeoutException as exc:
51
+ raise StackureError(
52
+ "timeout",
53
+ f"Request timed out after {_REQUEST_TIMEOUT_S}s",
54
+ ) from exc
55
+ except httpx.RequestError as exc:
56
+ last_error = StackureError("network", f"Network request failed: {exc}")
57
+
58
+ raise last_error or StackureError("network", "Request failed after retries")
59
+
60
+
61
+ def _handle_response(response: httpx.Response) -> dict:
62
+ """Return parsed JSON or raise a typed :class:`StackureError` for non-2xx."""
63
+ if not response.is_success:
64
+ try:
65
+ error_text = response.text
66
+ except Exception:
67
+ error_text = "unknown error"
68
+ if response.status_code == 401:
69
+ raise StackureError("auth", error_text or "Authentication failed", 401)
70
+ if response.status_code == 403:
71
+ raise StackureError("forbidden", error_text or "Access forbidden", 403)
72
+ raise StackureError(
73
+ "network",
74
+ f"API error ({response.status_code}): {error_text}",
75
+ response.status_code,
76
+ )
77
+ try:
78
+ return response.json()
79
+ except Exception as exc:
80
+ raise StackureError("network", "Invalid JSON response from server") from exc
81
+
82
+
83
+ async def send_magic_link(email: str, app_id: str | None = None) -> MagicLinkResponse:
84
+ """Send a passwordless sign-in email to a user.
85
+
86
+ Args:
87
+ email: Recipient's email address.
88
+ app_id: Your Stackure application UUID. Optional.
89
+
90
+ Returns:
91
+ :class:`MagicLinkResponse` with the API's confirmation message.
92
+
93
+ Raises:
94
+ StackureError: With ``code`` in ``{"validation", "network", "timeout", "auth"}``.
95
+ """
96
+ validate_email(email)
97
+ if app_id:
98
+ validate_uuid(app_id, "App ID")
99
+ body: dict = {"user_email": email}
100
+ if app_id:
101
+ body["app_id"] = app_id
102
+ response = await _request("POST", "/api/public/auth/magic-link/send", json=body)
103
+ data = _handle_response(response)
104
+ try:
105
+ return MagicLinkResponse(message=data["message"])
106
+ except KeyError as exc:
107
+ raise StackureError("network", "Unexpected API response format") from exc
108
+
109
+
110
+ async def _validate_session(app_id: str, cookies: dict | None = None) -> dict:
111
+ """Internal helper: validate a session and return the raw API response dict."""
112
+ validate_uuid(app_id, "App ID")
113
+ response = await _request(
114
+ "GET",
115
+ "/api/public/auth/session/validate",
116
+ params={"app_id": app_id},
117
+ cookies=cookies,
118
+ )
119
+ data = _handle_response(response)
120
+ user = None
121
+ if data.get("user"):
122
+ u = data["user"]
123
+ try:
124
+ user = User(
125
+ user_id=u["user_id"],
126
+ user_email=u["user_email"],
127
+ user_first_name=u["user_first_name"],
128
+ user_last_name=u["user_last_name"],
129
+ user_roles=u.get("user_roles", []),
130
+ )
131
+ except KeyError as exc:
132
+ raise StackureError("network", "Unexpected user payload format") from exc
133
+ return {
134
+ "authenticated": data.get("authenticated", False),
135
+ "user": user,
136
+ "sign_in_url": data.get("sign_in_url"),
137
+ }
138
+
139
+
140
+ async def logout(cookies: dict | None = None) -> None:
141
+ """Revoke the session represented by ``cookies``.
142
+
143
+ Args:
144
+ cookies: Cookies from the incoming HTTP request.
145
+
146
+ Raises:
147
+ StackureError: With ``code`` in ``{"network", "timeout"}``.
148
+ """
149
+ response = await _request("POST", "/api/public/auth/sign-out", cookies=cookies)
150
+ _handle_response(response)
stackure/errors.py ADDED
@@ -0,0 +1,26 @@
1
+ """Stackure SDK error type."""
2
+
3
+
4
+ class StackureError(Exception):
5
+ """The single exception type raised by every SDK function.
6
+
7
+ Catch once and inspect ``code`` to branch on category.
8
+
9
+ Attributes:
10
+ code: One of ``"validation"``, ``"auth"``, ``"forbidden"``, ``"timeout"``,
11
+ ``"network"``.
12
+ status_code: HTTP status returned by the API, or ``None`` if the error
13
+ happened before a response was received.
14
+
15
+ Example:
16
+ >>> try:
17
+ ... await send_magic_link(email="bad")
18
+ ... except StackureError as err:
19
+ ... if err.code == "validation":
20
+ ... ...
21
+ """
22
+
23
+ def __init__(self, code: str, message: str, status_code: int | None = None):
24
+ super().__init__(message)
25
+ self.code = code
26
+ self.status_code = status_code
stackure/middleware.py ADDED
@@ -0,0 +1,148 @@
1
+ """Authentication verification and decorator for web frameworks."""
2
+
3
+ import asyncio
4
+ import functools
5
+ import inspect
6
+ import logging
7
+ from collections.abc import Callable
8
+ from typing import Any
9
+
10
+ from .client import _validate_session
11
+ from .errors import StackureError
12
+ from .types import VerifyResult
13
+
14
+ _logger = logging.getLogger(__name__)
15
+
16
+
17
+ async def verify(
18
+ app_id: str,
19
+ cookies: dict | None = None,
20
+ roles: list[str] | None = None,
21
+ ) -> VerifyResult:
22
+ """Check whether ``cookies`` hold a valid session for ``app_id``.
23
+
24
+ Returns a :class:`VerifyResult` without raising; callers inspect the
25
+ result and decide how to respond.
26
+
27
+ Args:
28
+ app_id: Your Stackure application UUID.
29
+ cookies: Session cookies from the incoming HTTP request.
30
+ roles: Optional required roles. The user must hold at least one.
31
+
32
+ Example:
33
+ >>> result = await verify(app_id="...", cookies=dict(request.cookies))
34
+ >>> if not result.authenticated:
35
+ ... return {"error": result.error["message"]}, result.error["code"]
36
+ >>> return {"user": result.user}
37
+ """
38
+ try:
39
+ session = await _validate_session(app_id, cookies)
40
+ if not session["authenticated"] or not session["user"]:
41
+ return VerifyResult(
42
+ authenticated=False,
43
+ error={
44
+ "code": 401,
45
+ "message": "Valid authentication required",
46
+ "sign_in_url": session["sign_in_url"],
47
+ },
48
+ )
49
+ user = session["user"]
50
+ if roles and not any(role in user.user_roles for role in roles):
51
+ return VerifyResult(
52
+ authenticated=False,
53
+ user=user,
54
+ error={"code": 403, "message": f"Requires one of: {', '.join(roles)}"},
55
+ )
56
+ return VerifyResult(authenticated=True, user=user)
57
+ except Exception as e:
58
+ _logger.error("stackure: verification error: %s", e)
59
+ return VerifyResult(
60
+ authenticated=False,
61
+ error={"code": 500, "message": "Authentication verification failed"},
62
+ )
63
+
64
+
65
+ def _extract_cookies(request: Any) -> dict:
66
+ """Pull cookies off any framework's request object.
67
+
68
+ Works with FastAPI, Starlette, Flask, aiohttp (``cookies`` attr) and
69
+ Django (``COOKIES`` attr). Returns an empty dict if nothing is found.
70
+ """
71
+ if request is None:
72
+ return {}
73
+ cookies = getattr(request, "cookies", None) or getattr(request, "COOKIES", None)
74
+ return dict(cookies) if cookies else {}
75
+
76
+
77
+ def auth(app_id: str, roles: list[str] | None = None) -> Callable:
78
+ """Decorator enforcing authentication on a view.
79
+
80
+ On success, attaches the authenticated :class:`~stackure.User` to
81
+ ``request.user`` (when the request object accepts attribute assignment).
82
+ On failure, raises :class:`~stackure.StackureError` — your framework's
83
+ exception handling turns it into the appropriate HTTP response.
84
+
85
+ Supports both synchronous and asynchronous view functions.
86
+
87
+ Args:
88
+ app_id: Your Stackure application UUID.
89
+ roles: Optional required roles.
90
+
91
+ Example:
92
+ >>> @app.get("/dashboard")
93
+ ... @auth(app_id="your-app-id", roles=["admin"])
94
+ ... async def dashboard(request):
95
+ ... return {"user": request.user}
96
+ """
97
+
98
+ def decorator(func: Callable) -> Callable:
99
+ if inspect.iscoroutinefunction(func):
100
+
101
+ @functools.wraps(func)
102
+ async def _async_wrapper(*args: Any, **kwargs: Any) -> Any:
103
+ request = args[0] if args else kwargs.get("request")
104
+ result = await verify(
105
+ app_id,
106
+ cookies=_extract_cookies(request),
107
+ roles=roles,
108
+ )
109
+ if not result.authenticated:
110
+ _raise(result)
111
+ try:
112
+ request.user = result.user
113
+ except (AttributeError, TypeError):
114
+ pass
115
+ return await func(*args, **kwargs)
116
+
117
+ return _async_wrapper
118
+
119
+ @functools.wraps(func)
120
+ def _sync_wrapper(*args: Any, **kwargs: Any) -> Any:
121
+ request = args[0] if args else kwargs.get("request")
122
+ loop = asyncio.new_event_loop()
123
+ try:
124
+ result = loop.run_until_complete(
125
+ verify(app_id, cookies=_extract_cookies(request), roles=roles),
126
+ )
127
+ finally:
128
+ loop.close()
129
+ if not result.authenticated:
130
+ _raise(result)
131
+ try:
132
+ request.user = result.user
133
+ except (AttributeError, TypeError):
134
+ pass
135
+ return func(*args, **kwargs)
136
+
137
+ return _sync_wrapper
138
+
139
+ return decorator
140
+
141
+
142
+ def _raise(result: VerifyResult) -> None:
143
+ """Translate a failed :class:`VerifyResult` into a :class:`StackureError`."""
144
+ code = result.error["code"] if result.error else 401
145
+ message = result.error["message"] if result.error else "Authentication required"
146
+ if code == 403:
147
+ raise StackureError("forbidden", message, 403)
148
+ raise StackureError("auth", message, code)
stackure/py.typed ADDED
File without changes
stackure/types.py ADDED
@@ -0,0 +1,52 @@
1
+ """Data types returned by the Stackure SDK."""
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass
7
+ class User:
8
+ """An authenticated Stackure user.
9
+
10
+ Attributes:
11
+ user_id: Unique identifier for the user.
12
+ user_email: User's email address.
13
+ user_first_name: User's first name.
14
+ user_last_name: User's last name.
15
+ user_roles: Role names assigned to the user for the current app.
16
+ """
17
+
18
+ user_id: str
19
+ user_email: str
20
+ user_first_name: str
21
+ user_last_name: str
22
+ user_roles: list[str] = field(default_factory=list)
23
+
24
+
25
+ @dataclass
26
+ class MagicLinkResponse:
27
+ """Successful :func:`send_magic_link` response.
28
+
29
+ Attributes:
30
+ message: Human-readable confirmation string from the API.
31
+ """
32
+
33
+ message: str
34
+
35
+
36
+ @dataclass
37
+ class VerifyResult:
38
+ """Outcome of a :func:`verify` call.
39
+
40
+ Exactly one of ``user`` or ``error`` is populated depending on
41
+ ``authenticated``.
42
+
43
+ Attributes:
44
+ authenticated: Whether the request carries a valid session.
45
+ user: Authenticated user when ``authenticated`` is ``True``.
46
+ error: Dict with ``code`` (int), ``message`` (str), and optional
47
+ ``sign_in_url`` when ``authenticated`` is ``False``.
48
+ """
49
+
50
+ authenticated: bool
51
+ user: User | None = None
52
+ error: dict | None = None
stackure/validation.py ADDED
@@ -0,0 +1,30 @@
1
+ """Input validation utilities for the Stackure SDK."""
2
+
3
+ import re
4
+
5
+ from .errors import StackureError
6
+
7
+ _EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
8
+ _UUID_RE = re.compile(
9
+ r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
10
+ re.IGNORECASE,
11
+ )
12
+
13
+
14
+ def validate_email(email: str) -> None:
15
+ """Raise a ``"validation"``-coded :class:`StackureError` if ``email`` is malformed."""
16
+ if not email or not isinstance(email, str):
17
+ raise StackureError("validation", "email is required")
18
+ if not _EMAIL_RE.match(email):
19
+ raise StackureError("validation", "invalid email format")
20
+
21
+
22
+ def validate_uuid(value: str, field_name: str = "UUID") -> None:
23
+ """Raise a ``"validation"``-coded :class:`StackureError` if ``value`` isn't a UUID v4."""
24
+ if not value or not isinstance(value, str):
25
+ raise StackureError("validation", f"{field_name} is required")
26
+ if not _UUID_RE.match(value):
27
+ raise StackureError(
28
+ "validation",
29
+ f"invalid {field_name} format (must be a valid UUID)",
30
+ )
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: stackure
3
+ Version: 1.0.0
4
+ Summary: Official Stackure authentication SDK for Python
5
+ Project-URL: Homepage, https://stackure.com
6
+ Project-URL: Repository, https://github.com/syi-stackure/sdk-py
7
+ Project-URL: Documentation, https://docs.stackure.com
8
+ Project-URL: Bug Tracker, https://github.com/syi-stackure/sdk-py/issues
9
+ Author-email: Stackure <support@stackure.com>
10
+ License: MIT License
11
+
12
+ Copyright (c) 2025 ysashank
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: auth,authentication,magic-link,passwordless,sdk,sso,stackure,verify
33
+ Classifier: Development Status :: 5 - Production/Stable
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Programming Language :: Python :: 3.13
41
+ Classifier: Topic :: Internet :: WWW/HTTP
42
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
43
+ Classifier: Typing :: Typed
44
+ Requires-Python: >=3.10
45
+ Requires-Dist: httpx
46
+ Description-Content-Type: text/markdown
47
+
48
+ # Stackure Python SDK
49
+
50
+ [![Check build](https://github.com/syi-stackure/sdk-py/actions/workflows/check-build.yml/badge.svg)](https://github.com/syi-stackure/sdk-py/actions/workflows/check-build.yml)
51
+ [![PyPI version](https://img.shields.io/pypi/v/stackure.svg)](https://pypi.org/project/stackure/)
52
+ [![Python versions](https://img.shields.io/pypi/pyversions/stackure.svg)](https://pypi.org/project/stackure/)
53
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/stackure.svg)](https://pypi.org/project/stackure/)
54
+ [![Trusted publisher](https://img.shields.io/badge/pypi-trusted--publisher-blue)](https://docs.pypi.org/trusted-publishers/)
55
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
56
+
57
+ Authentication for your app. One decorator.
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install stackure
63
+ ```
64
+
65
+ Requires Python 3.10+.
66
+
67
+ ## Protect a route
68
+
69
+ ```python
70
+ from stackure import auth
71
+
72
+ @app.get("/admin")
73
+ @auth(app_id="my-app-id", roles=["admin"])
74
+ async def admin(request):
75
+ return {"user": request.user}
76
+ ```
77
+
78
+ Works with FastAPI, Starlette, Django, Flask, aiohttp — cookies extracted automatically from the request object.
79
+
80
+ ## Verify manually
81
+
82
+ ```python
83
+ from stackure import verify
84
+
85
+ result = await verify(app_id="my-app-id", cookies=dict(request.cookies))
86
+
87
+ if not result.authenticated:
88
+ return {"error": result.error["message"]}, result.error["code"]
89
+
90
+ return {"user": result.user}
91
+ ```
92
+
93
+ ## Send a magic link
94
+
95
+ ```python
96
+ from stackure import send_magic_link
97
+
98
+ await send_magic_link(email="user@example.com", app_id="my-app-id")
99
+ ```
100
+
101
+ ## Log out
102
+
103
+ ```python
104
+ from stackure import logout
105
+
106
+ await logout(dict(request.cookies))
107
+ ```
108
+
109
+ ## Configuration
110
+
111
+ Set `STACKURE_BASE_URL` to point at a non-production environment:
112
+
113
+ ```bash
114
+ STACKURE_BASE_URL=https://stage.stackure.com python app.py
115
+ ```
116
+
117
+ ## Errors
118
+
119
+ All errors are `StackureError`. Switch on `.code`:
120
+
121
+ ```python
122
+ from stackure import StackureError
123
+
124
+ try:
125
+ await send_magic_link(email=email)
126
+ except StackureError as err:
127
+ # err.code is one of: "validation" | "auth" | "forbidden" | "timeout" | "network"
128
+ ...
129
+ ```
130
+
131
+ ## Contributing
132
+
133
+ Open a PR. Tag a release when ready: `git tag vX.Y.Z && git push --tags` — the release workflow builds, signs, and publishes.
134
+
135
+ ## Security
136
+
137
+ Report vulnerabilities via [GitHub Security Advisories](https://github.com/syi-stackure/sdk-py/security/advisories/new). Releases publish to PyPI via [OIDC trusted publishing](https://docs.pypi.org/trusted-publishers/) with [GitHub build-provenance attestations](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds).
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,11 @@
1
+ stackure/__init__.py,sha256=iWvXzcYR9EYzqYbsTg0p5sxDiq6Fw46apNINTmE6Y5k,931
2
+ stackure/client.py,sha256=AltOmTvTLzdEnokbstPyisYTMTwoBJ-cjKtcoYLJNLI,5225
3
+ stackure/errors.py,sha256=d_0bpAQXfsDZeKv-pTCKZw-PUv-W2jSWH0tJKS336BM,827
4
+ stackure/middleware.py,sha256=pjNx65AuQOAe7cUFYo3bEelC1eAAh3RDIGAchIcHVxE,5190
5
+ stackure/types.py,sha256=WSahy_tMhQbAyTgGRK8XwUmpQo0m5MsVemSr-4f6LJE,1319
6
+ stackure/validation.py,sha256=yjOvrg9ljZuNKWCzy-RfR4FKn0iDBYhc8yOikAmwB9U,1069
7
+ stackure/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ stackure-1.0.0.dist-info/METADATA,sha256=IOtVa0uO1zfufp-UwdeKSnQrBbV-z4DMAl0iVoEfHaY,4978
9
+ stackure-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
10
+ stackure-1.0.0.dist-info/licenses/LICENSE,sha256=lcI5jP8yVxAC9CwlzJQi0f0mHv3Xy9ku_iJ-L8pU6ZY,1065
11
+ stackure-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ysashank
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.