xyberos-linear 0.1.0__tar.gz
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.
- xyberos_linear-0.1.0/PKG-INFO +56 -0
- xyberos_linear-0.1.0/README.md +46 -0
- xyberos_linear-0.1.0/pyproject.toml +23 -0
- xyberos_linear-0.1.0/setup.cfg +4 -0
- xyberos_linear-0.1.0/tests/test_client.py +48 -0
- xyberos_linear-0.1.0/tests/test_plugin.py +26 -0
- xyberos_linear-0.1.0/xyberos_linear/__init__.py +6 -0
- xyberos_linear-0.1.0/xyberos_linear/client.py +70 -0
- xyberos_linear-0.1.0/xyberos_linear/http.py +57 -0
- xyberos_linear-0.1.0/xyberos_linear/plugin.py +60 -0
- xyberos_linear-0.1.0/xyberos_linear.egg-info/PKG-INFO +56 -0
- xyberos_linear-0.1.0/xyberos_linear.egg-info/SOURCES.txt +14 -0
- xyberos_linear-0.1.0/xyberos_linear.egg-info/dependency_links.txt +1 -0
- xyberos_linear-0.1.0/xyberos_linear.egg-info/entry_points.txt +2 -0
- xyberos_linear-0.1.0/xyberos_linear.egg-info/requires.txt +1 -0
- xyberos_linear-0.1.0/xyberos_linear.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xyberos-linear
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Linear GraphQL plugin (RFC-0019, M7): search and create issues
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: xyberos,plugin,linear,tool
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: xyberos>=1.0
|
|
10
|
+
|
|
11
|
+
# xyberos-linear
|
|
12
|
+
|
|
13
|
+
**Linear GraphQL plugin — RFC-0019, M7 (community wave).** Search and create
|
|
14
|
+
issues from Xyberos agents via the Linear GraphQL API. Stdlib-only with an
|
|
15
|
+
injectable transport for tests.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -e ./linear
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from xyberos import create_app
|
|
27
|
+
from xyberos_linear import LinearPlugin
|
|
28
|
+
|
|
29
|
+
app = create_app()
|
|
30
|
+
app.load_plugin(LinearPlugin()) # key from LINEAR_API_KEY
|
|
31
|
+
|
|
32
|
+
app.tools.execute("linear_search_issues", None, query="bug", first=10)
|
|
33
|
+
app.tools.execute("linear_create_issue", None, team_id="TEAM_ID", title="New issue", description="...")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Tools
|
|
37
|
+
|
|
38
|
+
| Tool | Notes |
|
|
39
|
+
| ---- | ----- |
|
|
40
|
+
| `linear_search_issues(query="", first=10)` | search by title |
|
|
41
|
+
| `linear_create_issue(team_id, title, description="")` | create an issue |
|
|
42
|
+
|
|
43
|
+
Requires `LINEAR_API_KEY` (sent as the `Authorization` header).
|
|
44
|
+
|
|
45
|
+
## Tests
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install pytest
|
|
49
|
+
pytest tests/
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Canned responses via an injectable transport — no network.
|
|
53
|
+
|
|
54
|
+
## Ship location
|
|
55
|
+
|
|
56
|
+
Plugin (`xyberos.plugins` entry point) — community wave (M7).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# xyberos-linear
|
|
2
|
+
|
|
3
|
+
**Linear GraphQL plugin — RFC-0019, M7 (community wave).** Search and create
|
|
4
|
+
issues from Xyberos agents via the Linear GraphQL API. Stdlib-only with an
|
|
5
|
+
injectable transport for tests.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e ./linear
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from xyberos import create_app
|
|
17
|
+
from xyberos_linear import LinearPlugin
|
|
18
|
+
|
|
19
|
+
app = create_app()
|
|
20
|
+
app.load_plugin(LinearPlugin()) # key from LINEAR_API_KEY
|
|
21
|
+
|
|
22
|
+
app.tools.execute("linear_search_issues", None, query="bug", first=10)
|
|
23
|
+
app.tools.execute("linear_create_issue", None, team_id="TEAM_ID", title="New issue", description="...")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Tools
|
|
27
|
+
|
|
28
|
+
| Tool | Notes |
|
|
29
|
+
| ---- | ----- |
|
|
30
|
+
| `linear_search_issues(query="", first=10)` | search by title |
|
|
31
|
+
| `linear_create_issue(team_id, title, description="")` | create an issue |
|
|
32
|
+
|
|
33
|
+
Requires `LINEAR_API_KEY` (sent as the `Authorization` header).
|
|
34
|
+
|
|
35
|
+
## Tests
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install pytest
|
|
39
|
+
pytest tests/
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Canned responses via an injectable transport — no network.
|
|
43
|
+
|
|
44
|
+
## Ship location
|
|
45
|
+
|
|
46
|
+
Plugin (`xyberos.plugins` entry point) — community wave (M7).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xyberos-linear"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Linear GraphQL plugin (RFC-0019, M7): search and create issues"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = {text = "Apache-2.0"}
|
|
12
|
+
dependencies = ["xyberos>=1.0"]
|
|
13
|
+
keywords = ["xyberos", "plugin", "linear", "tool"]
|
|
14
|
+
|
|
15
|
+
[project.entry-points."xyberos.plugins"]
|
|
16
|
+
linear = "xyberos_linear.plugin:plugin"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools]
|
|
19
|
+
packages = ["xyberos_linear"]
|
|
20
|
+
|
|
21
|
+
[tool.pytest.ini_options]
|
|
22
|
+
testpaths = ["tests"]
|
|
23
|
+
pythonpath = ["."]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Tests for the Linear GraphQL client (injectable transport, no network)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from xyberos.exceptions.provider import ProviderError
|
|
7
|
+
|
|
8
|
+
from xyberos_linear import LinearClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _fake_request():
|
|
12
|
+
def request(method, url, **kwargs):
|
|
13
|
+
body = kwargs.get("json_body", {})
|
|
14
|
+
if "issueCreate" in body.get("query", ""):
|
|
15
|
+
return 200, {"data": {"issueCreate": {"success": True, "issue": {"id": "i1", "url": "https://linear.app/x/ISSUE-1"}}}}
|
|
16
|
+
return 200, {"data": {"issues": {"nodes": [{"id": "i1", "identifier": "TEAM-1", "title": "Fix bug", "url": "https://linear.app/x/TEAM-1"}]}}}
|
|
17
|
+
|
|
18
|
+
return request
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_search_issues():
|
|
22
|
+
request = _fake_request()
|
|
23
|
+
client = LinearClient(api_key="k", request=request)
|
|
24
|
+
issues = client.search_issues("bug", first=5)
|
|
25
|
+
assert issues == [{"id": "i1", "identifier": "TEAM-1", "title": "Fix bug", "url": "https://linear.app/x/TEAM-1"}]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_create_issue():
|
|
29
|
+
request = _fake_request()
|
|
30
|
+
client = LinearClient(api_key="k", request=request)
|
|
31
|
+
result = client.create_issue("team1", "New issue", "details")
|
|
32
|
+
assert result == {"id": "i1", "url": "https://linear.app/x/ISSUE-1", "success": True}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_requires_api_key():
|
|
36
|
+
request = _fake_request()
|
|
37
|
+
client = LinearClient(api_key=None, request=request)
|
|
38
|
+
with pytest.raises(ProviderError, match="LINEAR_API_KEY"):
|
|
39
|
+
client.search_issues()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_graphql_error_raises():
|
|
43
|
+
def request(method, url, **kwargs):
|
|
44
|
+
return 200, {"errors": [{"message": "Unauthorized"}]}
|
|
45
|
+
|
|
46
|
+
client = LinearClient(api_key="k", request=request)
|
|
47
|
+
with pytest.raises(ProviderError, match="Unauthorized"):
|
|
48
|
+
client.search_issues()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Tests for loading the Linear plugin into a Xyberos app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from xyberos import create_app
|
|
6
|
+
|
|
7
|
+
from xyberos_linear import LinearPlugin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _fake_request():
|
|
11
|
+
def request(method, url, **kwargs):
|
|
12
|
+
return 200, {"data": {"issues": {"nodes": [{"id": "i1", "identifier": "TEAM-1", "title": "Fix bug", "url": "https://linear.app/x/TEAM-1"}]}}}
|
|
13
|
+
|
|
14
|
+
return request
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_plugin_registers_and_executes():
|
|
18
|
+
app = create_app()
|
|
19
|
+
app.load_plugin(LinearPlugin(api_key="k", request=_fake_request()))
|
|
20
|
+
assert "linear_search_issues" in app.tools.names
|
|
21
|
+
assert "linear_create_issue" in app.tools.names
|
|
22
|
+
|
|
23
|
+
issues = app.tools.execute("linear_search_issues", None, query="bug")
|
|
24
|
+
assert issues[0]["identifier"] == "TEAM-1"
|
|
25
|
+
|
|
26
|
+
app.unload_plugin("linear")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""A minimal Linear GraphQL client (stdlib ``urllib``, injectable transport)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from xyberos.exceptions.provider import ProviderError
|
|
10
|
+
|
|
11
|
+
from .http import RequestTransport, default_request
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _raise_for_status(status: int, body: Any) -> None:
|
|
15
|
+
if 200 <= status < 300:
|
|
16
|
+
return
|
|
17
|
+
message = body if isinstance(body, str) else str(body)
|
|
18
|
+
raise ProviderError(f"Linear API returned HTTP {status}: {message[:200]}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LinearClient:
|
|
22
|
+
"""Thin client for the Linear GraphQL API."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
api_key: str | None = None,
|
|
27
|
+
*,
|
|
28
|
+
base_url: str = "https://api.linear.app/graphql",
|
|
29
|
+
request: RequestTransport | None = None,
|
|
30
|
+
timeout: float = 30.0,
|
|
31
|
+
) -> None:
|
|
32
|
+
self._api_key = api_key if api_key is not None else os.getenv("LINEAR_API_KEY")
|
|
33
|
+
self._base_url = base_url
|
|
34
|
+
self._request = request or default_request
|
|
35
|
+
self._timeout = timeout
|
|
36
|
+
|
|
37
|
+
def search_issues(self, query: str = "", *, first: int = 10) -> list[dict[str, Any]]:
|
|
38
|
+
filter_clause = f'filter: {{title: {{contains: {json.dumps(query)}}}}}, ' if query else ""
|
|
39
|
+
graphql = f"{{ issues({filter_clause}first: {first}) {{ nodes {{ id identifier title url }} }} }}"
|
|
40
|
+
data = self._graphql(graphql)
|
|
41
|
+
return [
|
|
42
|
+
{"id": node.get("id"), "identifier": node.get("identifier"), "title": node.get("title"), "url": node.get("url")}
|
|
43
|
+
for node in data.get("issues", {}).get("nodes", [])
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def create_issue(self, team_id: str, title: str, description: str = "") -> dict[str, Any]:
|
|
47
|
+
graphql = (
|
|
48
|
+
"mutation { issueCreate(input: {"
|
|
49
|
+
f'teamId: {json.dumps(team_id)}, title: {json.dumps(title)}, description: {json.dumps(description)}'
|
|
50
|
+
"}) { success issue { id url } } }"
|
|
51
|
+
)
|
|
52
|
+
data = self._graphql(graphql)
|
|
53
|
+
created = data.get("issueCreate", {})
|
|
54
|
+
issue = created.get("issue") or {}
|
|
55
|
+
return {"id": issue.get("id"), "url": issue.get("url"), "success": created.get("success")}
|
|
56
|
+
|
|
57
|
+
def _graphql(self, query: str) -> dict[str, Any]:
|
|
58
|
+
if not self._api_key:
|
|
59
|
+
raise ProviderError("Linear requires an API key (set LINEAR_API_KEY)")
|
|
60
|
+
status, body = self._request(
|
|
61
|
+
"POST",
|
|
62
|
+
self._base_url,
|
|
63
|
+
json_body={"query": query},
|
|
64
|
+
headers={"Authorization": self._api_key},
|
|
65
|
+
timeout=self._timeout,
|
|
66
|
+
)
|
|
67
|
+
_raise_for_status(status, body)
|
|
68
|
+
if body.get("errors"):
|
|
69
|
+
raise ProviderError(f"Linear GraphQL error: {body['errors']}")
|
|
70
|
+
return body.get("data") or {}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""A tiny stdlib HTTP helper (no third-party deps).
|
|
2
|
+
|
|
3
|
+
``default_request`` performs one HTTP request with ``urllib`` and returns
|
|
4
|
+
``(status, body)`` where ``body`` is parsed JSON when the response is JSON,
|
|
5
|
+
otherwise raw text. Injectable so tests run without a network.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
from typing import Any, Callable
|
|
15
|
+
|
|
16
|
+
#: (method, url, *, json_body, headers, query, timeout) -> (status, body)
|
|
17
|
+
RequestTransport = Callable[..., tuple[int, Any]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def default_request(
|
|
21
|
+
method: str,
|
|
22
|
+
url: str,
|
|
23
|
+
*,
|
|
24
|
+
json_body: Any = None,
|
|
25
|
+
headers: dict[str, str] | None = None,
|
|
26
|
+
query: dict[str, Any] | None = None,
|
|
27
|
+
timeout: float = 30.0,
|
|
28
|
+
) -> tuple[int, Any]:
|
|
29
|
+
"""Send one request and return ``(status, parsed_json_or_text)``."""
|
|
30
|
+
final_url = url
|
|
31
|
+
if query:
|
|
32
|
+
separator = "&" if "?" in url else "?"
|
|
33
|
+
final_url = url + separator + urllib.parse.urlencode(query)
|
|
34
|
+
|
|
35
|
+
data: bytes | None = None
|
|
36
|
+
request_headers = dict(headers or {})
|
|
37
|
+
if json_body is not None:
|
|
38
|
+
data = json.dumps(json_body).encode("utf-8")
|
|
39
|
+
request_headers.setdefault("Content-Type", "application/json")
|
|
40
|
+
|
|
41
|
+
request = urllib.request.Request(
|
|
42
|
+
final_url, data=data, headers=request_headers, method=method
|
|
43
|
+
)
|
|
44
|
+
try:
|
|
45
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
46
|
+
raw = response.read()
|
|
47
|
+
content_type = response.headers.get("Content-Type", "")
|
|
48
|
+
except urllib.error.HTTPError as exc:
|
|
49
|
+
return exc.code, exc.read().decode("utf-8", errors="replace")
|
|
50
|
+
|
|
51
|
+
text = raw.decode("utf-8", errors="replace")
|
|
52
|
+
if "application/json" in content_type or text.lstrip().startswith("{"):
|
|
53
|
+
try:
|
|
54
|
+
return 200, json.loads(text)
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
return 200, text
|
|
57
|
+
return 200, text
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Linear plugin entry point (RFC-0019, M7)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
from xyberos.contracts import Plugin, Tool
|
|
8
|
+
from xyberos.tools import FunctionTool
|
|
9
|
+
|
|
10
|
+
from .client import LinearClient
|
|
11
|
+
from .http import RequestTransport
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _pop_tool(registry: Any, name: str) -> None:
|
|
15
|
+
unregister = getattr(registry, "unregister", None)
|
|
16
|
+
if callable(unregister):
|
|
17
|
+
unregister(name)
|
|
18
|
+
return
|
|
19
|
+
store = getattr(registry, "_tools", None)
|
|
20
|
+
if isinstance(store, dict):
|
|
21
|
+
cast(dict[str, Any], store).pop(name, None)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LinearPlugin(Plugin):
|
|
25
|
+
"""Registers Linear tools (search / create issues)."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, api_key: str | None = None, *, request: RequestTransport | None = None) -> None:
|
|
28
|
+
self._client = LinearClient(api_key, request=request)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def name(self) -> str:
|
|
32
|
+
return "linear"
|
|
33
|
+
|
|
34
|
+
def tools(self) -> list[Tool]:
|
|
35
|
+
client = self._client
|
|
36
|
+
|
|
37
|
+
def _search_issues(query: str = "", first: int = 10) -> list[dict[str, Any]]:
|
|
38
|
+
return client.search_issues(query, first=first)
|
|
39
|
+
|
|
40
|
+
def _create_issue(team_id: str, title: str, description: str = "") -> dict[str, Any]:
|
|
41
|
+
return client.create_issue(team_id, title, description)
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
FunctionTool("linear_search_issues", _search_issues, description="Search Linear issues by title."),
|
|
45
|
+
FunctionTool("linear_create_issue", _create_issue, description="Create a Linear issue in a team."),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
def register(self, kernel: object) -> None:
|
|
49
|
+
registry = kernel.resolve("tools")
|
|
50
|
+
for tool in self.tools():
|
|
51
|
+
registry.register(tool)
|
|
52
|
+
|
|
53
|
+
def unregister(self, kernel: object) -> None:
|
|
54
|
+
registry = kernel.resolve("tools")
|
|
55
|
+
for tool in self.tools():
|
|
56
|
+
_pop_tool(registry, tool.name)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
#: Auto-discovered by ``app.load_entry_points()``.
|
|
60
|
+
plugin = LinearPlugin()
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xyberos-linear
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Linear GraphQL plugin (RFC-0019, M7): search and create issues
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: xyberos,plugin,linear,tool
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: xyberos>=1.0
|
|
10
|
+
|
|
11
|
+
# xyberos-linear
|
|
12
|
+
|
|
13
|
+
**Linear GraphQL plugin — RFC-0019, M7 (community wave).** Search and create
|
|
14
|
+
issues from Xyberos agents via the Linear GraphQL API. Stdlib-only with an
|
|
15
|
+
injectable transport for tests.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install -e ./linear
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from xyberos import create_app
|
|
27
|
+
from xyberos_linear import LinearPlugin
|
|
28
|
+
|
|
29
|
+
app = create_app()
|
|
30
|
+
app.load_plugin(LinearPlugin()) # key from LINEAR_API_KEY
|
|
31
|
+
|
|
32
|
+
app.tools.execute("linear_search_issues", None, query="bug", first=10)
|
|
33
|
+
app.tools.execute("linear_create_issue", None, team_id="TEAM_ID", title="New issue", description="...")
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Tools
|
|
37
|
+
|
|
38
|
+
| Tool | Notes |
|
|
39
|
+
| ---- | ----- |
|
|
40
|
+
| `linear_search_issues(query="", first=10)` | search by title |
|
|
41
|
+
| `linear_create_issue(team_id, title, description="")` | create an issue |
|
|
42
|
+
|
|
43
|
+
Requires `LINEAR_API_KEY` (sent as the `Authorization` header).
|
|
44
|
+
|
|
45
|
+
## Tests
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install pytest
|
|
49
|
+
pytest tests/
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Canned responses via an injectable transport — no network.
|
|
53
|
+
|
|
54
|
+
## Ship location
|
|
55
|
+
|
|
56
|
+
Plugin (`xyberos.plugins` entry point) — community wave (M7).
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
tests/test_client.py
|
|
4
|
+
tests/test_plugin.py
|
|
5
|
+
xyberos_linear/__init__.py
|
|
6
|
+
xyberos_linear/client.py
|
|
7
|
+
xyberos_linear/http.py
|
|
8
|
+
xyberos_linear/plugin.py
|
|
9
|
+
xyberos_linear.egg-info/PKG-INFO
|
|
10
|
+
xyberos_linear.egg-info/SOURCES.txt
|
|
11
|
+
xyberos_linear.egg-info/dependency_links.txt
|
|
12
|
+
xyberos_linear.egg-info/entry_points.txt
|
|
13
|
+
xyberos_linear.egg-info/requires.txt
|
|
14
|
+
xyberos_linear.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xyberos>=1.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
xyberos_linear
|