meshapi-code 0.4.0__py3-none-any.whl → 0.4.1__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.
- meshapi/__init__.py +1 -1
- meshapi/attachments.py +150 -0
- meshapi/cli.py +36 -1
- meshapi/commands.py +45 -10
- meshapi/tools.py +3 -0
- {meshapi_code-0.4.0.dist-info → meshapi_code-0.4.1.dist-info}/METADATA +1 -1
- meshapi_code-0.4.1.dist-info/RECORD +19 -0
- meshapi_code-0.4.0.dist-info/RECORD +0 -18
- {meshapi_code-0.4.0.dist-info → meshapi_code-0.4.1.dist-info}/WHEEL +0 -0
- {meshapi_code-0.4.0.dist-info → meshapi_code-0.4.1.dist-info}/entry_points.txt +0 -0
- {meshapi_code-0.4.0.dist-info → meshapi_code-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {meshapi_code-0.4.0.dist-info → meshapi_code-0.4.1.dist-info}/licenses/NOTICE +0 -0
meshapi/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.4.
|
|
1
|
+
__version__ = "0.4.1"
|
meshapi/attachments.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Image attachment loading for multimodal chat.
|
|
2
|
+
|
|
3
|
+
Builds OpenAI-compatible content parts (`{type: image_url, image_url: {url,
|
|
4
|
+
detail}}`) from local file paths or HTTP(S) URLs. We always base64-encode
|
|
5
|
+
into a `data:image/...;base64,...` URL — the Mesh API docs warn that some
|
|
6
|
+
providers refuse public URLs and require base64, so always-base64 is the
|
|
7
|
+
maximally-compatible default.
|
|
8
|
+
|
|
9
|
+
Also provides a conservative auto-detector for the main loop: scan a user
|
|
10
|
+
prompt for tokens that unambiguously look like image paths or URLs (start
|
|
11
|
+
with `/`, `~`, `./`, `../`, or `http(s)://`, end in a known image extension),
|
|
12
|
+
so a dragged-in file path can be attached without an explicit slash command.
|
|
13
|
+
"""
|
|
14
|
+
import base64
|
|
15
|
+
import mimetypes
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from urllib.parse import urlparse
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
|
|
21
|
+
# Size guardrails. We don't refuse — vision tokens are the user's call — but we
|
|
22
|
+
# do report sizes back so the user sees the cost.
|
|
23
|
+
HARD_LIMIT_BYTES = 20 * 1024 * 1024 # 20 MB
|
|
24
|
+
|
|
25
|
+
IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".gif", ".webp")
|
|
26
|
+
IMAGE_MIMES = {"image/png", "image/jpeg", "image/gif", "image/webp"}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class AttachmentError(Exception):
|
|
30
|
+
"""Raised for any user-facing failure to attach an image."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_image(source: str, detail: str = "auto") -> tuple[dict, dict]:
|
|
34
|
+
"""Load an image from a local path or URL into a content part.
|
|
35
|
+
|
|
36
|
+
Returns (content_part, info):
|
|
37
|
+
- content_part is the dict to splice into the message's `content` array:
|
|
38
|
+
`{"type":"image_url","image_url":{"url":"data:...","detail":...}}`
|
|
39
|
+
- info has display metadata: `{"name", "size_bytes", "mime"}`
|
|
40
|
+
|
|
41
|
+
Raises AttachmentError on any failure (not found, too big, wrong MIME,
|
|
42
|
+
network error). Caller is expected to surface the message to the user.
|
|
43
|
+
"""
|
|
44
|
+
if _looks_like_url(source):
|
|
45
|
+
data, mime, name = _fetch_url(source)
|
|
46
|
+
else:
|
|
47
|
+
data, mime, name = _read_local(source)
|
|
48
|
+
|
|
49
|
+
if len(data) > HARD_LIMIT_BYTES:
|
|
50
|
+
raise AttachmentError(
|
|
51
|
+
f"image too large: {len(data) // (1024*1024)} MB "
|
|
52
|
+
f"(limit {HARD_LIMIT_BYTES // (1024*1024)} MB)"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
b64 = base64.b64encode(data).decode("ascii")
|
|
56
|
+
data_url = f"data:{mime};base64,{b64}"
|
|
57
|
+
return (
|
|
58
|
+
{"type": "image_url", "image_url": {"url": data_url, "detail": detail}},
|
|
59
|
+
{"name": name, "size_bytes": len(data), "mime": mime},
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def find_image_tokens(text: str) -> list[str]:
|
|
64
|
+
"""Return tokens in `text` that look like image paths or URLs.
|
|
65
|
+
|
|
66
|
+
Conservative on purpose — only matches:
|
|
67
|
+
- http(s) URLs ending in a known image extension
|
|
68
|
+
- Local paths starting with `/`, `~/`, `./`, or `../` (and ending in an
|
|
69
|
+
image extension, AND pointing at an existing file)
|
|
70
|
+
|
|
71
|
+
Bare filenames (`foo.png`) are NOT matched: too ambiguous with filenames
|
|
72
|
+
mentioned in conversation. Tokens wrapped in backticks or quotes are
|
|
73
|
+
skipped (user's escape hatch).
|
|
74
|
+
|
|
75
|
+
Trailing sentence punctuation (`.,;:!?)`) is trimmed before matching.
|
|
76
|
+
"""
|
|
77
|
+
matches: list[str] = []
|
|
78
|
+
for raw in text.split():
|
|
79
|
+
if not raw or raw[0] in "`\"'":
|
|
80
|
+
continue
|
|
81
|
+
token = raw
|
|
82
|
+
while token and token[-1] in ".,;:!?)":
|
|
83
|
+
token = token[:-1]
|
|
84
|
+
if not token:
|
|
85
|
+
continue
|
|
86
|
+
low = token.lower()
|
|
87
|
+
if low.startswith(("http://", "https://")):
|
|
88
|
+
path_part = token.split("?", 1)[0]
|
|
89
|
+
if path_part.lower().endswith(IMAGE_EXTS):
|
|
90
|
+
matches.append(token)
|
|
91
|
+
continue
|
|
92
|
+
if token.startswith(("/", "~/", "./", "../")):
|
|
93
|
+
if low.endswith(IMAGE_EXTS):
|
|
94
|
+
try:
|
|
95
|
+
p = Path(token).expanduser()
|
|
96
|
+
if p.is_file():
|
|
97
|
+
matches.append(token)
|
|
98
|
+
except OSError:
|
|
99
|
+
pass
|
|
100
|
+
return matches
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _looks_like_url(s: str) -> bool:
|
|
104
|
+
try:
|
|
105
|
+
u = urlparse(s)
|
|
106
|
+
except (ValueError, AttributeError):
|
|
107
|
+
return False
|
|
108
|
+
return u.scheme in ("http", "https") and bool(u.netloc)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _fetch_url(url: str) -> tuple[bytes, str, str]:
|
|
112
|
+
try:
|
|
113
|
+
with httpx.Client(timeout=30, follow_redirects=True) as client:
|
|
114
|
+
r = client.get(url)
|
|
115
|
+
r.raise_for_status()
|
|
116
|
+
data = r.content
|
|
117
|
+
mime = r.headers.get("content-type", "").split(";")[0].strip()
|
|
118
|
+
except httpx.HTTPError as e:
|
|
119
|
+
raise AttachmentError(f"couldn't fetch {url}: {e}") from None
|
|
120
|
+
|
|
121
|
+
if not mime:
|
|
122
|
+
guessed, _ = mimetypes.guess_type(url)
|
|
123
|
+
mime = guessed or ""
|
|
124
|
+
if mime not in IMAGE_MIMES:
|
|
125
|
+
raise AttachmentError(
|
|
126
|
+
f"URL doesn't look like an image (mime: {mime or 'unknown'})"
|
|
127
|
+
)
|
|
128
|
+
name = Path(urlparse(url).path).name or "image"
|
|
129
|
+
return data, mime, name
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _read_local(path_str: str) -> tuple[bytes, str, str]:
|
|
133
|
+
path = Path(path_str).expanduser()
|
|
134
|
+
if not path.exists():
|
|
135
|
+
raise AttachmentError(f"file not found: {path}")
|
|
136
|
+
if not path.is_file():
|
|
137
|
+
raise AttachmentError(f"not a regular file: {path}")
|
|
138
|
+
mime, _ = mimetypes.guess_type(str(path))
|
|
139
|
+
if not mime:
|
|
140
|
+
raise AttachmentError(f"couldn't determine MIME type for {path.name}")
|
|
141
|
+
if mime not in IMAGE_MIMES:
|
|
142
|
+
raise AttachmentError(
|
|
143
|
+
f"{path.name} isn't an image (mime: {mime}). "
|
|
144
|
+
"For text documents, use /file."
|
|
145
|
+
)
|
|
146
|
+
try:
|
|
147
|
+
data = path.read_bytes()
|
|
148
|
+
except OSError as e:
|
|
149
|
+
raise AttachmentError(f"can't read {path}: {e}") from None
|
|
150
|
+
return data, mime, path.name
|
meshapi/cli.py
CHANGED
|
@@ -23,6 +23,7 @@ from rich.markup import escape as _rich_escape
|
|
|
23
23
|
from rich.text import Text
|
|
24
24
|
|
|
25
25
|
from . import __version__, statusbar
|
|
26
|
+
from .attachments import AttachmentError, find_image_tokens, load_image
|
|
26
27
|
from .client import stream_chat
|
|
27
28
|
from .commands import handle_command
|
|
28
29
|
from .config import CONFIG_FILE, HISTORY_FILE, load_config, secure_file
|
|
@@ -681,6 +682,7 @@ def main() -> None:
|
|
|
681
682
|
"mode": from_str(args.mode),
|
|
682
683
|
"plan": None, # populated by the model via create_plan
|
|
683
684
|
"servers": [], # background processes spawned via start_server
|
|
685
|
+
"pending_attachments": [], # image content parts queued via /image
|
|
684
686
|
}
|
|
685
687
|
|
|
686
688
|
# Mode cycle — used by both the prompt-toolkit keybinding (while at the
|
|
@@ -786,7 +788,40 @@ def main() -> None:
|
|
|
786
788
|
break
|
|
787
789
|
continue
|
|
788
790
|
|
|
789
|
-
|
|
791
|
+
# Auto-detect image paths/URLs in the prompt and attach them. Tokens
|
|
792
|
+
# that look like unambiguous image paths (start with /, ~, ./, ../,
|
|
793
|
+
# or http(s)://) and end in a known image extension are pulled out
|
|
794
|
+
# and replaced with "[Image #N]" so the text reads naturally.
|
|
795
|
+
auto_text = user_input
|
|
796
|
+
auto_parts: list = []
|
|
797
|
+
queued = state.get("pending_attachments") or []
|
|
798
|
+
n_offset = len(queued)
|
|
799
|
+
for token in find_image_tokens(user_input):
|
|
800
|
+
if token not in auto_text:
|
|
801
|
+
continue # already replaced (duplicate mention)
|
|
802
|
+
try:
|
|
803
|
+
part, info = load_image(token)
|
|
804
|
+
except AttachmentError as e:
|
|
805
|
+
console.print(f"[yellow]Couldn't auto-attach {token}: {e}[/yellow]")
|
|
806
|
+
continue
|
|
807
|
+
n = n_offset + len(auto_parts) + 1
|
|
808
|
+
auto_text = auto_text.replace(token, f"[Image #{n}]")
|
|
809
|
+
auto_parts.append(part)
|
|
810
|
+
size_kb = max(1, info["size_bytes"] // 1024)
|
|
811
|
+
console.print(
|
|
812
|
+
f"[{CODE}]📎 attached {info['name']} ({size_kb} KB, {info['mime']})[/{CODE}]"
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
all_parts = queued + auto_parts
|
|
816
|
+
if all_parts:
|
|
817
|
+
console.print(f"[dim]→ sending {len(all_parts)} image(s) with this prompt[/dim]")
|
|
818
|
+
state["messages"].append({
|
|
819
|
+
"role": "user",
|
|
820
|
+
"content": [{"type": "text", "text": auto_text}] + all_parts,
|
|
821
|
+
})
|
|
822
|
+
state["pending_attachments"] = []
|
|
823
|
+
else:
|
|
824
|
+
state["messages"].append({"role": "user", "content": user_input})
|
|
790
825
|
console.print()
|
|
791
826
|
|
|
792
827
|
# Tool-calling loop: keep streaming until model returns text without
|
meshapi/commands.py
CHANGED
|
@@ -3,9 +3,10 @@ from pathlib import Path
|
|
|
3
3
|
|
|
4
4
|
from rich.panel import Panel
|
|
5
5
|
|
|
6
|
+
from .attachments import AttachmentError, load_image
|
|
6
7
|
from .config import save_config
|
|
7
8
|
from .permissions import LABELS, Mode, from_str
|
|
8
|
-
from .render import console, fmt_usd
|
|
9
|
+
from .render import CODE, console, fmt_usd
|
|
9
10
|
from .tools import build_system_prompt
|
|
10
11
|
|
|
11
12
|
ROUTES = {"cheapest", "fastest", "balanced"}
|
|
@@ -55,6 +56,35 @@ def handle_command(cmd: str, state: dict) -> bool:
|
|
|
55
56
|
else:
|
|
56
57
|
console.print(f"[red]File not found: {path}[/red]")
|
|
57
58
|
|
|
59
|
+
elif name == "/image":
|
|
60
|
+
if not arg:
|
|
61
|
+
queued = state.get("pending_attachments") or []
|
|
62
|
+
if not queued:
|
|
63
|
+
console.print(
|
|
64
|
+
"[dim]/image <path-or-url> attach an image to the next prompt[/dim]"
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
console.print(f"[dim]{len(queued)} image(s) queued for next prompt[/dim]")
|
|
68
|
+
else:
|
|
69
|
+
try:
|
|
70
|
+
part, info = load_image(arg.strip())
|
|
71
|
+
except AttachmentError as e:
|
|
72
|
+
console.print(f"[red]Can't attach: {e}[/red]")
|
|
73
|
+
else:
|
|
74
|
+
state.setdefault("pending_attachments", []).append(part)
|
|
75
|
+
size_kb = max(1, info["size_bytes"] // 1024)
|
|
76
|
+
console.print(
|
|
77
|
+
f"[{CODE}]📎 attached {info['name']} ({size_kb} KB, {info['mime']})[/{CODE}]"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
elif name == "/clear-attach":
|
|
81
|
+
had = len(state.get("pending_attachments") or [])
|
|
82
|
+
state["pending_attachments"] = []
|
|
83
|
+
if had:
|
|
84
|
+
console.print(f"[dim]Dropped {had} queued attachment(s).[/dim]")
|
|
85
|
+
else:
|
|
86
|
+
console.print("[dim]Nothing queued.[/dim]")
|
|
87
|
+
|
|
58
88
|
elif name == "/system":
|
|
59
89
|
if arg:
|
|
60
90
|
state["cfg"]["system"] = arg
|
|
@@ -79,15 +109,20 @@ def handle_command(cmd: str, state: dict) -> bool:
|
|
|
79
109
|
|
|
80
110
|
elif name == "/help":
|
|
81
111
|
console.print(Panel.fit(
|
|
82
|
-
"/exit
|
|
83
|
-
"/clear
|
|
84
|
-
"/model <name>
|
|
85
|
-
"/route <mode>
|
|
86
|
-
"/mode <perm>
|
|
87
|
-
"/file <path>
|
|
88
|
-
"/
|
|
89
|
-
"/
|
|
90
|
-
"/
|
|
112
|
+
"/exit end session\n"
|
|
113
|
+
"/clear reset conversation\n"
|
|
114
|
+
"/model <name> switch model (e.g. anthropic/claude-sonnet-4.5)\n"
|
|
115
|
+
"/route <mode> cheapest|fastest|balanced|default\n"
|
|
116
|
+
"/mode <perm> ask|bypass|none (or shift+tab to cycle)\n"
|
|
117
|
+
"/file <path> add text file to context\n"
|
|
118
|
+
"/image <path|url> attach an image (base64) to the next prompt\n"
|
|
119
|
+
"/clear-attach drop any queued image attachments\n"
|
|
120
|
+
"/system <txt> set system prompt\n"
|
|
121
|
+
"/cost show session spend\n"
|
|
122
|
+
"/help show this\n\n"
|
|
123
|
+
"[dim]Image paths in a prompt auto-attach: drop /path/img.png in your\n"
|
|
124
|
+
"input and it's sent as a base64 image part. Wrap in backticks to keep\n"
|
|
125
|
+
"it as text. Multiple images per prompt are supported.[/dim]",
|
|
91
126
|
title="commands",
|
|
92
127
|
border_style="cyan",
|
|
93
128
|
))
|
meshapi/tools.py
CHANGED
|
@@ -39,6 +39,9 @@ def build_system_prompt(cfg: dict) -> str:
|
|
|
39
39
|
"pass flags like --yes, -y, or --no-input; interactive prompts will "
|
|
40
40
|
"hang and time out. The shell timeout is 120s; if a command would "
|
|
41
41
|
"take longer, break it into smaller pieces.\n\n"
|
|
42
|
+
"User messages may include images attached as multimodal content "
|
|
43
|
+
"parts (image_url with a data: or https: URL). Look at them carefully "
|
|
44
|
+
"and reference what you see when relevant.\n\n"
|
|
42
45
|
"For long-running servers (dev servers like `npm run dev` / `vite` / "
|
|
43
46
|
"`next dev`, `flask run`, `python -m http.server`, file watchers, etc.) "
|
|
44
47
|
"use the start_server tool — NOT run_bash. run_bash will kill the "
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
meshapi/__init__.py,sha256=pMtTmSUht-XtbR_7Doz6bsQqopJJd8rZ8I8zy2HwwoA,22
|
|
2
|
+
meshapi/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
|
3
|
+
meshapi/attachments.py,sha256=Mpsxm66QT_cJV4TXlnYU23ZhFm4vdzFEyXDenXjxpEU,5475
|
|
4
|
+
meshapi/cli.py,sha256=N2uqztGWVCP3z6_kkPKUieWIiTrg187LN5Zy5640e_k,37447
|
|
5
|
+
meshapi/client.py,sha256=Rtc-8W9XncxPlV6qQ9I_c25BizyBHYNiIy8Eb3kSaEw,2920
|
|
6
|
+
meshapi/commands.py,sha256=MdaXgpfWdkg0bDE-Q-ufin0Wivcu66LOlJ2nzdvNqco,5302
|
|
7
|
+
meshapi/config.py,sha256=DKFljJh1DfSispptYA7mtJFBVMzE8MMyb5UvcelxwTY,2349
|
|
8
|
+
meshapi/keywatcher.py,sha256=tWVSLWZY-p08CcOd10Xvf5TrMGfjDaKDzYJRSfe4kPo,8057
|
|
9
|
+
meshapi/permissions.py,sha256=BPLYiPrlLR1js9k64szm9b11fXYx0ZZcQ2a08GLNRg8,1033
|
|
10
|
+
meshapi/plan.py,sha256=JWgzm2Qtbdso7nnoR7K896d7n7ufwlhT-2F09PGXXKs,2561
|
|
11
|
+
meshapi/render.py,sha256=VwgDbYSElwEJ0WhSMpRZ8Tw_EA0A09s8D4yVh_nUL3o,4737
|
|
12
|
+
meshapi/statusbar.py,sha256=yqF6fzCaZMXMzUmX1vzmKWAMbCe_YRsbnA27meA3vaw,1361
|
|
13
|
+
meshapi/tools.py,sha256=OKJkLiJMDBiHss4FymoysrK7QsVP3yHhdh_4Mmmk9f4,13036
|
|
14
|
+
meshapi_code-0.4.1.dist-info/METADATA,sha256=yegzqYueiE0cOvtC6DWirpfV3Vn9f___2tYs4_VmyKs,7595
|
|
15
|
+
meshapi_code-0.4.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
16
|
+
meshapi_code-0.4.1.dist-info/entry_points.txt,sha256=ZCXZ_SgrhWIQEHSjAXz0pUlyGbIQKZ68vp_Cg1Y0rME,45
|
|
17
|
+
meshapi_code-0.4.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
18
|
+
meshapi_code-0.4.1.dist-info/licenses/NOTICE,sha256=wF-6Apse4eVIOpbNP3WLtTaOJClNFK7Jok2BnUvSo9U,191
|
|
19
|
+
meshapi_code-0.4.1.dist-info/RECORD,,
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
meshapi/__init__.py,sha256=42STGor_9nKYXumfeV5tiyD_M8VdcddX7CEexmibPBk,22
|
|
2
|
-
meshapi/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
|
|
3
|
-
meshapi/cli.py,sha256=wLtLmRFgN9AQ7Z050I0hCfIQKcIgM3-kjy0CNcoq5Nw,35752
|
|
4
|
-
meshapi/client.py,sha256=Rtc-8W9XncxPlV6qQ9I_c25BizyBHYNiIy8Eb3kSaEw,2920
|
|
5
|
-
meshapi/commands.py,sha256=fc_qnTa_oZgn5CRQ7GrpUm1Zclmat4McLdEV5YX5Wvg,3662
|
|
6
|
-
meshapi/config.py,sha256=DKFljJh1DfSispptYA7mtJFBVMzE8MMyb5UvcelxwTY,2349
|
|
7
|
-
meshapi/keywatcher.py,sha256=tWVSLWZY-p08CcOd10Xvf5TrMGfjDaKDzYJRSfe4kPo,8057
|
|
8
|
-
meshapi/permissions.py,sha256=BPLYiPrlLR1js9k64szm9b11fXYx0ZZcQ2a08GLNRg8,1033
|
|
9
|
-
meshapi/plan.py,sha256=JWgzm2Qtbdso7nnoR7K896d7n7ufwlhT-2F09PGXXKs,2561
|
|
10
|
-
meshapi/render.py,sha256=VwgDbYSElwEJ0WhSMpRZ8Tw_EA0A09s8D4yVh_nUL3o,4737
|
|
11
|
-
meshapi/statusbar.py,sha256=yqF6fzCaZMXMzUmX1vzmKWAMbCe_YRsbnA27meA3vaw,1361
|
|
12
|
-
meshapi/tools.py,sha256=6KXuoRW5-XwIkGfLd-v2Ylr7UqVePEOwZFyCP6CT20M,12825
|
|
13
|
-
meshapi_code-0.4.0.dist-info/METADATA,sha256=qEXN-tvjfzPDGPck2aX_arFb74_eoCF3fHBUtp6MrU0,7595
|
|
14
|
-
meshapi_code-0.4.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
15
|
-
meshapi_code-0.4.0.dist-info/entry_points.txt,sha256=ZCXZ_SgrhWIQEHSjAXz0pUlyGbIQKZ68vp_Cg1Y0rME,45
|
|
16
|
-
meshapi_code-0.4.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
17
|
-
meshapi_code-0.4.0.dist-info/licenses/NOTICE,sha256=wF-6Apse4eVIOpbNP3WLtTaOJClNFK7Jok2BnUvSo9U,191
|
|
18
|
-
meshapi_code-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|