aops 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.
aops/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """aops — LangChain integration for AgentOps.
2
+
3
+ Quick start::
4
+
5
+ import aops
6
+ aops.init(api_key="aops_aHR0cDovL2xvY2FsaG9zdDo4MDAw_...")
7
+
8
+ from aops.langchain import pull, chain_prompt
9
+ """
10
+
11
+ from aops._client import AopsClient
12
+ from aops._config import init
13
+ from aops._exceptions import (
14
+ AgentNotFoundError,
15
+ AopsConnectionError,
16
+ AopsError,
17
+ ChainNotFoundError,
18
+ VersionNotFoundError,
19
+ )
20
+ from aops._keys import generate_key, parse_key
21
+
22
+ __all__ = [
23
+ "init",
24
+ "AopsClient",
25
+ "generate_key",
26
+ "parse_key",
27
+ "AopsError",
28
+ "AgentNotFoundError",
29
+ "ChainNotFoundError",
30
+ "VersionNotFoundError",
31
+ "AopsConnectionError",
32
+ ]
aops/_cache.py ADDED
@@ -0,0 +1,33 @@
1
+ import time
2
+ from typing import Any
3
+
4
+
5
+ class TTLCache:
6
+ """Simple in-memory cache with TTL expiration."""
7
+
8
+ def __init__(self, ttl: int = 300) -> None:
9
+ self._ttl = ttl
10
+ self._store: dict[str, tuple[Any, float]] = {}
11
+
12
+ def get(self, key: str) -> Any | None:
13
+ if self._ttl == 0:
14
+ return None
15
+ entry = self._store.get(key)
16
+ if entry is None:
17
+ return None
18
+ value, ts = entry
19
+ if time.monotonic() - ts > self._ttl:
20
+ del self._store[key]
21
+ return None
22
+ return value
23
+
24
+ def set(self, key: str, value: Any) -> None:
25
+ if self._ttl == 0:
26
+ return
27
+ self._store[key] = (value, time.monotonic())
28
+
29
+ def invalidate(self, key: str) -> None:
30
+ self._store.pop(key, None)
31
+
32
+ def clear(self) -> None:
33
+ self._store.clear()
aops/_client.py ADDED
@@ -0,0 +1,147 @@
1
+ import uuid
2
+
3
+ import httpx
4
+
5
+ from aops._cache import TTLCache
6
+ from aops._config import get_config
7
+ from aops._exceptions import (
8
+ AgentNotFoundError,
9
+ AopsConnectionError,
10
+ ChainNotFoundError,
11
+ VersionNotFoundError,
12
+ )
13
+ from aops._keys import InvalidApiKeyError, parse_key
14
+ from aops._models import AgentModel, ChainModel, ChainVersionModel
15
+
16
+
17
+ class AopsClient:
18
+ """HTTP client for the AgentOps backend API."""
19
+
20
+ def __init__(
21
+ self,
22
+ api_key: str | None = None,
23
+ *,
24
+ base_url: str | None = None,
25
+ api_prefix: str | None = None,
26
+ cache_ttl: int | None = None,
27
+ ) -> None:
28
+ if api_key is not None or base_url is not None:
29
+ # Explicit construction — resolve host from key, then allow override
30
+ resolved_url = base_url or _host_from_key(api_key)
31
+ self._api_base = f"{resolved_url.rstrip('/')}{api_prefix or '/api/v1'}"
32
+ self._api_key = api_key
33
+ ttl = cache_ttl if cache_ttl is not None else 300
34
+ else:
35
+ config = get_config()
36
+ self._api_base = config.api_base
37
+ self._api_key = config.api_key
38
+ ttl = cache_ttl if cache_ttl is not None else config.cache_ttl
39
+
40
+ self._cache = TTLCache(ttl)
41
+
42
+ # ------------------------------------------------------------------
43
+ # Internal helpers
44
+ # ------------------------------------------------------------------
45
+
46
+ def _headers(self) -> dict[str, str]:
47
+ if self._api_key:
48
+ return {"Authorization": f"Bearer {self._api_key}"}
49
+ return {}
50
+
51
+ def _get(self, path: str) -> list | dict:
52
+ url = f"{self._api_base}{path}"
53
+ try:
54
+ with httpx.Client() as client:
55
+ response = client.get(url, headers=self._headers())
56
+ response.raise_for_status()
57
+ return response.json()
58
+ except httpx.ConnectError as exc:
59
+ raise AopsConnectionError(
60
+ f"Cannot reach AgentOps at '{self._api_base}'. "
61
+ "Check that the backend is running and your API key is correct."
62
+ ) from exc
63
+ except httpx.HTTPStatusError as exc:
64
+ status = exc.response.status_code
65
+ if status == 401:
66
+ raise AopsConnectionError(
67
+ "AgentOps rejected the API key (401 Unauthorized). "
68
+ "Check that AGENTOPS_API_KEY is valid."
69
+ ) from exc
70
+ if status == 403:
71
+ raise AopsConnectionError(
72
+ "Access denied (403 Forbidden). "
73
+ "This API key may not have access to the requested agent."
74
+ ) from exc
75
+ raise AopsConnectionError(
76
+ f"AgentOps returned {status} for {url}"
77
+ ) from exc
78
+
79
+ # ------------------------------------------------------------------
80
+ # Public API
81
+ # ------------------------------------------------------------------
82
+
83
+ def get_agent_by_name(self, agent_name: str) -> AgentModel:
84
+ cache_key = f"agent:{agent_name}"
85
+ cached = self._cache.get(cache_key)
86
+ if cached is not None:
87
+ return cached
88
+
89
+ agents = [AgentModel(**a) for a in self._get("/agents/")]
90
+ for agent in agents:
91
+ if agent.name == agent_name:
92
+ self._cache.set(cache_key, agent)
93
+ return agent
94
+
95
+ raise AgentNotFoundError(
96
+ f"Agent '{agent_name}' not found. "
97
+ f"Available agents: {[a.name for a in agents] or '(none)'}"
98
+ )
99
+
100
+ def get_chain_by_name(self, agent_id: uuid.UUID, chain_name: str) -> ChainModel:
101
+ cache_key = f"chain:{agent_id}:{chain_name}"
102
+ cached = self._cache.get(cache_key)
103
+ if cached is not None:
104
+ return cached
105
+
106
+ chains = [ChainModel(**c) for c in self._get(f"/agents/{agent_id}/chains/")]
107
+ for chain in chains:
108
+ if chain.name == chain_name:
109
+ self._cache.set(cache_key, chain)
110
+ return chain
111
+
112
+ raise ChainNotFoundError(
113
+ f"Chain '{chain_name}' not found. "
114
+ f"Available chains: {[c.name for c in chains] or '(none)'}"
115
+ )
116
+
117
+ def get_chain_version(
118
+ self, agent_id: uuid.UUID, chain_id: uuid.UUID, version_number: int
119
+ ) -> ChainVersionModel:
120
+ cache_key = f"version:{chain_id}:{version_number}"
121
+ cached = self._cache.get(cache_key)
122
+ if cached is not None:
123
+ return cached
124
+
125
+ versions = [
126
+ ChainVersionModel(**v)
127
+ for v in self._get(f"/agents/{agent_id}/chains/{chain_id}/versions/")
128
+ ]
129
+ for version in versions:
130
+ if version.version_number == version_number:
131
+ self._cache.set(cache_key, version)
132
+ return version
133
+
134
+ available = sorted(v.version_number for v in versions)
135
+ raise VersionNotFoundError(
136
+ f"Version {version_number} not found. Available versions: {available or '(none)'}"
137
+ )
138
+
139
+
140
+ def _host_from_key(api_key: str | None) -> str:
141
+ if api_key is None:
142
+ return "http://localhost:8000"
143
+ try:
144
+ host, _ = parse_key(api_key)
145
+ return host
146
+ except InvalidApiKeyError:
147
+ return "http://localhost:8000"
aops/_config.py ADDED
@@ -0,0 +1,90 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+
4
+ from aops._keys import InvalidApiKeyError, parse_key
5
+
6
+
7
+ @dataclass
8
+ class Config:
9
+ base_url: str = "http://localhost:8000"
10
+ api_prefix: str = "/api/v1"
11
+ api_key: str | None = None
12
+ cache_ttl: int = 300 # seconds; 0 = no cache
13
+
14
+ @property
15
+ def api_base(self) -> str:
16
+ return f"{self.base_url.rstrip('/')}{self.api_prefix}"
17
+
18
+
19
+ _config: Config | None = None
20
+
21
+
22
+ def init(
23
+ api_key: str | None = None,
24
+ *,
25
+ base_url: str | None = None,
26
+ api_prefix: str | None = None,
27
+ cache_ttl: int | None = None,
28
+ ) -> None:
29
+ """Configure the AgentOps connection.
30
+
31
+ Preferred usage — key only (host is parsed from the key automatically)::
32
+
33
+ aops.init(api_key="aops_aHR0cDovL2xvY2FsaG9zdDo4MDAw_...")
34
+
35
+ Manual URL override (e.g. for testing or reverse-proxy setups)::
36
+
37
+ aops.init(api_key="aops_...", base_url="http://my-proxy:9000")
38
+
39
+ Environment variables (when ``init()`` is not called explicitly):
40
+ AGENTOPS_API_KEY — the API key (host is parsed from it)
41
+ AGENTOPS_BASE_URL — overrides the host embedded in the key
42
+ AGENTOPS_API_PREFIX — default: /api/v1
43
+ AGENTOPS_CACHE_TTL — default: 300 (seconds)
44
+ """
45
+ global _config
46
+
47
+ resolved_key = api_key or os.getenv("AGENTOPS_API_KEY")
48
+ explicit_url = base_url or os.getenv("AGENTOPS_BASE_URL")
49
+
50
+ resolved_url = _resolve_base_url(resolved_key, explicit_url)
51
+
52
+ _config = Config(
53
+ base_url=resolved_url,
54
+ api_prefix=api_prefix or os.getenv("AGENTOPS_API_PREFIX", "/api/v1"),
55
+ api_key=resolved_key,
56
+ cache_ttl=cache_ttl if cache_ttl is not None else int(os.getenv("AGENTOPS_CACHE_TTL", "300")),
57
+ )
58
+
59
+
60
+ def get_config() -> Config:
61
+ global _config
62
+ if _config is None:
63
+ resolved_key = os.getenv("AGENTOPS_API_KEY")
64
+ explicit_url = os.getenv("AGENTOPS_BASE_URL")
65
+ _config = Config(
66
+ base_url=_resolve_base_url(resolved_key, explicit_url),
67
+ api_prefix=os.getenv("AGENTOPS_API_PREFIX", "/api/v1"),
68
+ api_key=resolved_key,
69
+ cache_ttl=int(os.getenv("AGENTOPS_CACHE_TTL", "300")),
70
+ )
71
+ return _config
72
+
73
+
74
+ # ------------------------------------------------------------------
75
+ # Internal helpers
76
+ # ------------------------------------------------------------------
77
+
78
+ def _resolve_base_url(api_key: str | None, explicit_url: str | None) -> str:
79
+ """Determine the base URL from the key and/or an explicit override."""
80
+ if explicit_url:
81
+ return explicit_url
82
+
83
+ if api_key:
84
+ try:
85
+ host, _ = parse_key(api_key)
86
+ return host
87
+ except InvalidApiKeyError:
88
+ pass # fall through to default
89
+
90
+ return "http://localhost:8000"
aops/_exceptions.py ADDED
@@ -0,0 +1,18 @@
1
+ class AopsError(Exception):
2
+ """Base exception for aops."""
3
+
4
+
5
+ class AgentNotFoundError(AopsError):
6
+ """Agent with the given name does not exist."""
7
+
8
+
9
+ class ChainNotFoundError(AopsError):
10
+ """Chain with the given name does not exist."""
11
+
12
+
13
+ class VersionNotFoundError(AopsError):
14
+ """The requested chain version does not exist."""
15
+
16
+
17
+ class AopsConnectionError(AopsError):
18
+ """Failed to connect to the AgentOps backend."""
aops/_keys.py ADDED
@@ -0,0 +1,83 @@
1
+ """API key utilities for aops.
2
+
3
+ Key format:
4
+ aops_{base64url(host)}_{secret_token}
5
+
6
+ Example:
7
+ host = "http://localhost:8000"
8
+ key = "aops_aHR0cDovL2xvY2FsaG9zdDo4MDAw_xK9mP2abcXYZ..."
9
+
10
+ The host is base64url-encoded (no padding) inside the key, so the library
11
+ can resolve the AgentOps server without a separate base_url setting.
12
+ """
13
+
14
+ import base64
15
+ import secrets
16
+
17
+ _PREFIX = "aops"
18
+ _SEP = "_"
19
+
20
+
21
+ class InvalidApiKeyError(ValueError):
22
+ """Raised when an API key cannot be parsed."""
23
+
24
+
25
+ def generate_key(host: str) -> str:
26
+ """Generate a new API key that embeds *host*.
27
+
28
+ This helper is intended for the AgentOps backend when issuing keys.
29
+ The returned key must be stored (hashed) and should be shown to the
30
+ user only once.
31
+
32
+ Args:
33
+ host: Full base URL of the AgentOps server,
34
+ e.g. ``"http://localhost:8000"``.
35
+
36
+ Returns:
37
+ A key string of the form ``"aops_{encoded_host}_{token}"``.
38
+ """
39
+ encoded_host = _encode_host(host)
40
+ token = secrets.token_urlsafe(32)
41
+ return f"{_PREFIX}{_SEP}{encoded_host}{_SEP}{token}"
42
+
43
+
44
+ def parse_key(api_key: str) -> tuple[str, str]:
45
+ """Parse an API key and return ``(host, token)``.
46
+
47
+ Args:
48
+ api_key: A key produced by :func:`generate_key`.
49
+
50
+ Returns:
51
+ ``(host, token)`` tuple where *host* is the raw base URL string.
52
+
53
+ Raises:
54
+ :class:`InvalidApiKeyError` if the key format is invalid.
55
+ """
56
+ parts = api_key.split(_SEP, 2)
57
+ if len(parts) != 3 or parts[0] != _PREFIX:
58
+ raise InvalidApiKeyError(
59
+ f"Invalid API key format. Expected 'aops_<host>_<token>', got: '{api_key[:12]}...'"
60
+ )
61
+ _, encoded_host, token = parts
62
+ try:
63
+ host = _decode_host(encoded_host)
64
+ except Exception as exc:
65
+ raise InvalidApiKeyError(
66
+ f"Cannot decode host from API key: {exc}"
67
+ ) from exc
68
+ return host, token
69
+
70
+
71
+ # ------------------------------------------------------------------
72
+ # Internal helpers
73
+ # ------------------------------------------------------------------
74
+
75
+ def _encode_host(host: str) -> str:
76
+ """base64url-encode *host* without padding."""
77
+ return base64.urlsafe_b64encode(host.encode()).decode().rstrip("=")
78
+
79
+
80
+ def _decode_host(encoded: str) -> str:
81
+ """Decode a base64url *encoded* host (handles missing padding)."""
82
+ padded = encoded + "=" * (-len(encoded) % 4)
83
+ return base64.urlsafe_b64decode(padded).decode()
aops/_models.py ADDED
@@ -0,0 +1,33 @@
1
+ import uuid
2
+ from datetime import datetime
3
+
4
+ from pydantic import BaseModel
5
+
6
+
7
+ class AgentModel(BaseModel):
8
+ id: uuid.UUID
9
+ name: str
10
+ description: str | None
11
+ created_at: datetime
12
+ updated_at: datetime
13
+
14
+
15
+ class ChainModel(BaseModel):
16
+ id: uuid.UUID
17
+ agent_id: uuid.UUID
18
+ name: str
19
+ description: str | None
20
+ persona: str
21
+ content: str
22
+ created_at: datetime
23
+ updated_at: datetime
24
+
25
+
26
+ class ChainVersionModel(BaseModel):
27
+ id: uuid.UUID
28
+ chain_id: uuid.UUID
29
+ persona: str
30
+ content: str
31
+ message: str
32
+ version_number: int
33
+ created_at: datetime
@@ -0,0 +1,3 @@
1
+ from aops.langchain._loader import chain_prompt, pull
2
+
3
+ __all__ = ["pull", "chain_prompt"]
@@ -0,0 +1,166 @@
1
+ import functools
2
+ import inspect
3
+ from typing import Callable
4
+
5
+ from langchain_core.prompts import SystemMessagePromptTemplate
6
+
7
+ from aops._client import AopsClient
8
+
9
+
10
+ def _to_system_prompt(persona: str, content: str) -> SystemMessagePromptTemplate:
11
+ """Convert AgentOps chain fields into a SystemMessagePromptTemplate.
12
+
13
+ Both ``persona`` and ``content`` are agent-authored system-level instructions,
14
+ merged into a single SystemMessage:
15
+
16
+ # Persona
17
+ <persona text>
18
+
19
+ # Content
20
+ <content text — may contain {template_variables}>
21
+
22
+ ``persona`` is treated as a fixed string (its braces are escaped).
23
+ ``content`` may contain LangChain template variables in single braces,
24
+ e.g. ``"Answer in {language}"``.
25
+ To include a literal brace in content use double braces: ``{{`` or ``}}``.
26
+ """
27
+ persona_escaped = persona.replace("{", "{{").replace("}", "}}")
28
+ system_text = f"# Persona\n{persona_escaped}\n\n# Content\n{content}"
29
+ return SystemMessagePromptTemplate.from_template(system_text)
30
+
31
+
32
+ def pull(
33
+ ref: str,
34
+ *,
35
+ version: int | None = None,
36
+ client: AopsClient | None = None,
37
+ ) -> SystemMessagePromptTemplate:
38
+ """Fetch a chain from AgentOps and return it as a SystemMessagePromptTemplate.
39
+
40
+ Args:
41
+ ref: ``"agent-name/chain-name"`` reference string.
42
+ version: Specific version number to load. ``None`` loads the current
43
+ (latest saved) chain content.
44
+ client: Optional pre-configured :class:`~aops._client.AopsClient`.
45
+ When omitted the global configuration is used.
46
+
47
+ Returns:
48
+ A :class:`langchain_core.prompts.SystemMessagePromptTemplate` combining
49
+ the chain's ``persona`` and ``content`` into a single system message.
50
+
51
+ Example::
52
+
53
+ from aops.langchain import pull
54
+
55
+ prompt = pull("customer-support/greeter")
56
+ prompt_v2 = pull("customer-support/greeter", version=2)
57
+
58
+ chain = prompt | ChatOpenAI() | StrOutputParser()
59
+ chain.invoke({"topic": "billing"})
60
+ """
61
+ if "/" not in ref:
62
+ raise ValueError(
63
+ f"Invalid ref '{ref}'. Expected format: 'agent-name/chain-name'."
64
+ )
65
+
66
+ agent_name, chain_name = ref.split("/", 1)
67
+ c = client or AopsClient()
68
+
69
+ agent = c.get_agent_by_name(agent_name)
70
+ chain = c.get_chain_by_name(agent.id, chain_name)
71
+
72
+ if version is not None:
73
+ v = c.get_chain_version(agent.id, chain.id, version)
74
+ return _to_system_prompt(v.persona, v.content)
75
+
76
+ return _to_system_prompt(chain.persona, chain.content)
77
+
78
+
79
+ def chain_prompt(
80
+ agent_name: str,
81
+ chain_name: str,
82
+ *,
83
+ version: int | None = None,
84
+ client: AopsClient | None = None,
85
+ ):
86
+ """Decorator that injects a ``SystemMessagePromptTemplate`` from AgentOps.
87
+
88
+ The prompt is fetched once on the first call and then served from the
89
+ client-level cache on subsequent calls.
90
+
91
+ **Function decorator** — the resolved prompt is passed as the first
92
+ positional argument::
93
+
94
+ @chain_prompt("my-agent", "summariser")
95
+ def summarise(prompt: SystemMessagePromptTemplate, text: str) -> str:
96
+ return (prompt | ChatOpenAI()).invoke({"text": text})
97
+
98
+ result = summarise(text="Long article...")
99
+
100
+ **Class decorator** — the resolved prompt is passed as the second
101
+ positional argument to ``__init__`` (after ``self``)::
102
+
103
+ @chain_prompt("my-agent", "summariser")
104
+ class Summariser:
105
+ def __init__(self, prompt: SystemMessagePromptTemplate) -> None:
106
+ self.chain = prompt | ChatOpenAI()
107
+
108
+ def run(self, text: str) -> str:
109
+ return self.chain.invoke({"text": text})
110
+
111
+ summariser = Summariser()
112
+ result = summariser.run(text="Long article...")
113
+
114
+ Args:
115
+ agent_name: Name of the agent registered in AgentOps.
116
+ chain_name: Name of the chain within that agent.
117
+ version: Version number to pin. ``None`` = latest.
118
+ client: Optional custom :class:`~aops._client.AopsClient`.
119
+ """
120
+
121
+ def decorator(target: type | Callable) -> type | Callable:
122
+ if inspect.isclass(target):
123
+ return _wrap_class(target, agent_name, chain_name, version, client)
124
+ return _wrap_function(target, agent_name, chain_name, version, client)
125
+
126
+ return decorator
127
+
128
+
129
+ # ------------------------------------------------------------------
130
+ # Internal wrappers
131
+ # ------------------------------------------------------------------
132
+
133
+ def _wrap_function(
134
+ func: Callable,
135
+ agent_name: str,
136
+ chain_name: str,
137
+ version: int | None,
138
+ client: AopsClient | None,
139
+ ) -> Callable:
140
+ ref = f"{agent_name}/{chain_name}"
141
+
142
+ @functools.wraps(func)
143
+ def wrapper(*args, **kwargs):
144
+ prompt = pull(ref, version=version, client=client)
145
+ return func(prompt, *args, **kwargs)
146
+
147
+ return wrapper
148
+
149
+
150
+ def _wrap_class(
151
+ cls: type,
152
+ agent_name: str,
153
+ chain_name: str,
154
+ version: int | None,
155
+ client: AopsClient | None,
156
+ ) -> type:
157
+ ref = f"{agent_name}/{chain_name}"
158
+ original_init = cls.__init__
159
+
160
+ @functools.wraps(original_init)
161
+ def new_init(self, *args, **kwargs):
162
+ prompt = pull(ref, version=version, client=client)
163
+ original_init(self, prompt, *args, **kwargs)
164
+
165
+ cls.__init__ = new_init
166
+ return cls
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: aops
3
+ Version: 0.1.0
4
+ Summary: LangChain integration library for AOps
5
+ Requires-Python: >=3.12
6
+ License-File: LICENSE
7
+ Requires-Dist: httpx>=0.27
8
+ Requires-Dist: pydantic>=2.0
9
+ Requires-Dist: langchain-core>=0.3
10
+ Requires-Dist: python-dotenv>=1.0
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.0; extra == "dev"
13
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
14
+ Requires-Dist: respx>=0.21; extra == "dev"
15
+ Dynamic: license-file
@@ -0,0 +1,14 @@
1
+ aops/__init__.py,sha256=0DoHwiVNCV_nQPwSjWfElKpg0YniWe9OgOvI92lYtVI,665
2
+ aops/_cache.py,sha256=p28W9OGK94dJdzPRUfdxHGxLpESIOFjJValitIMPmLI,854
3
+ aops/_client.py,sha256=fvJTgT6oid9ZlBKUOsWr10GzEnKC-shHyu0kLyYpwBk,5262
4
+ aops/_config.py,sha256=oS7e_I4TevUsLRWaVEe8p5byXxjUFIjHFggBTMw3pbQ,2764
5
+ aops/_exceptions.py,sha256=M51X8XcSLJUGAl6j0ftT4-Kw68rtXCH1Ar8vKEeCiwk,433
6
+ aops/_keys.py,sha256=hpHy9l8Rr4mVDXfYOFR4KSk1QYdyB9m0iR4ZQEiOJUY,2427
7
+ aops/_models.py,sha256=09JWGzuAi-SPN3lEsNbR8oKX3oFb2GIvqTcLKGRK1yY,594
8
+ aops/langchain/__init__.py,sha256=4eHY60BBd8rvy3LEHlBtw4yrMOiHww67-QRDrl3hkZU,90
9
+ aops/langchain/_loader.py,sha256=bXlX1ll67mgcBbSICJgPKSkHXAHjuv1pzhX21NigQDA,5330
10
+ aops-0.1.0.dist-info/licenses/LICENSE,sha256=HxkQT8PjlbSHf5It346-bFDw4UAUPSXgvDBQ_csOXDU,1067
11
+ aops-0.1.0.dist-info/METADATA,sha256=KhegFZmRAvgTBaM2SVwUbDn_UDNybl9m879WxwJGREM,447
12
+ aops-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
13
+ aops-0.1.0.dist-info/top_level.txt,sha256=EaJFL4yXv8nlbsIcOE1I_HshvjLc6fWDIPqJZ-HESfs,5
14
+ aops-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Park Kibum
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.
@@ -0,0 +1 @@
1
+ aops