hermes-plugin-tinyfish 0.3.3__tar.gz → 0.3.4__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.
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/CHANGELOG.md +10 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/PKG-INFO +7 -5
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/README.md +6 -4
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/rest_client.py +26 -2
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/setup_cli.py +88 -134
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/plugin.yaml +1 -1
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/pyproject.toml +1 -1
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_rest_client.py +47 -5
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_setup_cli.py +77 -111
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/__init__.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/aggregate_benchmark.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/generate_report.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/improve_description.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/package_skill.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/quick_validate.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/run_eval.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/run_loop.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/.agents/skills/skill-creator/scripts/utils.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/.gitignore +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/CODE_OF_CONDUCT.md +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/CONTRIBUTING.md +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/LICENSE +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/SECURITY.md +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/__init__.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/__init__.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/browser_provider.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/config.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/credit_policy.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/health.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/normalize.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/provider.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/routing_context.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/update_check.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/scripts/prepare_release.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/scripts/validate_release.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/conftest.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_browser_provider.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_credit_policy.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_health.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_live.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_normalize.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_prepare_release.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_provider.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_register.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_routing_context.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_update_check.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_validate_release.py +0 -0
- {hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_version.py +0 -0
|
@@ -7,6 +7,16 @@ and this project uses semantic versioning.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## [0.3.4] - 2026-08-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Replaced `hermes tinyfish usage` request-history output with the actual
|
|
15
|
+
TinyFish wallet balance, auto-reload, pending top-up, and billing-rate API.
|
|
16
|
+
Legacy-billing accounts now receive a clear explanation instead of request
|
|
17
|
+
history, and the command states that no documented historical-spend API is
|
|
18
|
+
available.
|
|
19
|
+
|
|
10
20
|
## [0.3.3] - 2026-08-12
|
|
11
21
|
|
|
12
22
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hermes-plugin-tinyfish
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: TinyFish Search, Fetch, plain-language routing, update awareness, and optional Browser for Hermes Agent
|
|
5
5
|
Project-URL: Homepage, https://github.com/gabeosx/hermes-plugin-tinyfish
|
|
6
6
|
Project-URL: Documentation, https://github.com/gabeosx/hermes-plugin-tinyfish#readme
|
|
@@ -300,10 +300,12 @@ hermes tinyfish doctor --live-paid
|
|
|
300
300
|
present. It does not mean the access or refresh token is valid.
|
|
301
301
|
- `/tinyfish-status` shows non-networked status inside CLI or gateway sessions;
|
|
302
302
|
`/tinyfish-status live` explicitly runs Search and Fetch checks.
|
|
303
|
-
- `usage` reads TinyFish
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
303
|
+
- `usage` reads TinyFish's wallet API and reports the available balance,
|
|
304
|
+
auto-reload state, pending top-ups, and per-product billing rates. TinyFish
|
|
305
|
+
does not expose historical aggregate spend through its documented APIs. For
|
|
306
|
+
legacy-billing accounts without a Metronome wallet, the command explains why
|
|
307
|
+
balance data is unavailable and links to the billing dashboard. Use
|
|
308
|
+
`usage --json` for machine-readable output; request history is never dumped.
|
|
307
309
|
- `doctor --live-paid` only creates and closes a TinyFish Browser session. It
|
|
308
310
|
refuses under `deny`, requests approval under `request`, and never prints
|
|
309
311
|
connection URLs or credentials.
|
|
@@ -268,10 +268,12 @@ hermes tinyfish doctor --live-paid
|
|
|
268
268
|
present. It does not mean the access or refresh token is valid.
|
|
269
269
|
- `/tinyfish-status` shows non-networked status inside CLI or gateway sessions;
|
|
270
270
|
`/tinyfish-status live` explicitly runs Search and Fetch checks.
|
|
271
|
-
- `usage` reads TinyFish
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
271
|
+
- `usage` reads TinyFish's wallet API and reports the available balance,
|
|
272
|
+
auto-reload state, pending top-ups, and per-product billing rates. TinyFish
|
|
273
|
+
does not expose historical aggregate spend through its documented APIs. For
|
|
274
|
+
legacy-billing accounts without a Metronome wallet, the command explains why
|
|
275
|
+
balance data is unavailable and links to the billing dashboard. Use
|
|
276
|
+
`usage --json` for machine-readable output; request history is never dumped.
|
|
275
277
|
- `doctor --live-paid` only creates and closes a TinyFish Browser session. It
|
|
276
278
|
refuses under `deny`, requests approval under `request`, and never prints
|
|
277
279
|
connection URLs or credentials.
|
{hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/rest_client.py
RENAMED
|
@@ -11,6 +11,7 @@ import httpx
|
|
|
11
11
|
SEARCH_URL = "https://api.search.tinyfish.ai"
|
|
12
12
|
FETCH_URL = "https://api.fetch.tinyfish.ai"
|
|
13
13
|
BROWSER_URL = "https://api.browser.tinyfish.ai"
|
|
14
|
+
WALLET_URL = "https://agent.tinyfish.ai/v1/wallet"
|
|
14
15
|
SEARCH_USAGE_URL = f"{SEARCH_URL}/usage"
|
|
15
16
|
FETCH_USAGE_URL = f"{FETCH_URL}/usage"
|
|
16
17
|
|
|
@@ -24,6 +25,10 @@ class TinyFishRestError(RuntimeError):
|
|
|
24
25
|
"""Raised for TinyFish REST transport or HTTP failures."""
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
class TinyFishWalletNotFound(TinyFishRestError):
|
|
29
|
+
"""Raised when an account uses legacy billing or has no wallet yet."""
|
|
30
|
+
|
|
31
|
+
|
|
27
32
|
def _headers(api_key: str) -> dict[str, str]:
|
|
28
33
|
return {"X-API-Key": api_key, "Accept": "application/json"}
|
|
29
34
|
|
|
@@ -237,7 +242,26 @@ def fetch_usage(*, api_key: str, timeout: float = 30.0) -> dict[str, Any]:
|
|
|
237
242
|
)
|
|
238
243
|
|
|
239
244
|
|
|
245
|
+
def wallet(*, api_key: str, timeout: float = 30.0) -> dict[str, Any]:
|
|
246
|
+
"""Return the caller's TinyFish wallet balance and billing rates."""
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
response = httpx.get(WALLET_URL, headers=_headers(api_key), timeout=timeout)
|
|
250
|
+
response.raise_for_status()
|
|
251
|
+
return cast(dict[str, Any], response.json())
|
|
252
|
+
except httpx.HTTPStatusError as exc:
|
|
253
|
+
if exc.response.status_code == 404:
|
|
254
|
+
raise TinyFishWalletNotFound(
|
|
255
|
+
"TinyFish has no wallet for this account (legacy billing or no Metronome customer yet)."
|
|
256
|
+
) from exc
|
|
257
|
+
_raise_http_error("TinyFish Wallet", exc)
|
|
258
|
+
except httpx.RequestError as exc:
|
|
259
|
+
raise TinyFishRestError(f"Could not reach TinyFish Wallet: {exc}") from exc
|
|
260
|
+
except ValueError as exc:
|
|
261
|
+
raise TinyFishRestError("TinyFish Wallet returned invalid JSON") from exc
|
|
262
|
+
|
|
263
|
+
|
|
240
264
|
def usage(*, api_key: str, timeout: float = 30.0) -> dict[str, Any]:
|
|
241
|
-
"""Compatibility wrapper for the
|
|
265
|
+
"""Compatibility wrapper for the wallet-backed usage command."""
|
|
242
266
|
|
|
243
|
-
return
|
|
267
|
+
return wallet(api_key=api_key, timeout=timeout)
|
{hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/setup_cli.py
RENAMED
|
@@ -6,7 +6,6 @@ import argparse
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import sys
|
|
9
|
-
from collections.abc import Callable
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
from typing import Any, cast
|
|
12
11
|
|
|
@@ -35,6 +34,7 @@ from .routing_context import tinyfish_mcp_configured
|
|
|
35
34
|
from .update_check import InstallInfo, UpdateChecker
|
|
36
35
|
|
|
37
36
|
MCP_LOGIN_COMMAND = ("hermes", "mcp", "login", "tinyfish")
|
|
37
|
+
TINYFISH_BILLING_URL = "https://agent.tinyfish.ai/api-keys"
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def setup_tinyfish_cli(parser: argparse.ArgumentParser) -> None:
|
|
@@ -86,7 +86,7 @@ def setup_tinyfish_cli(parser: argparse.ArgumentParser) -> None:
|
|
|
86
86
|
credits_set.add_argument("policy", choices=list(CREDIT_POLICIES))
|
|
87
87
|
credits_sub.add_parser("reset", help="Reset Browser to deny and remove retired Agent/Profile policy keys")
|
|
88
88
|
|
|
89
|
-
usage = sub.add_parser("usage", help="Read TinyFish
|
|
89
|
+
usage = sub.add_parser("usage", help="Read TinyFish wallet balance and billing rates")
|
|
90
90
|
usage.add_argument("--json", action="store_true", help="Print machine-readable JSON")
|
|
91
91
|
|
|
92
92
|
|
|
@@ -702,133 +702,64 @@ def cmd_credits(args: argparse.Namespace) -> int:
|
|
|
702
702
|
return 2
|
|
703
703
|
|
|
704
704
|
|
|
705
|
-
def
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
return {
|
|
712
|
-
|
|
713
|
-
return {"success": False, "error": str(exc)}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
_USAGE_FIELD_ORDER = (
|
|
717
|
-
"created_at",
|
|
718
|
-
"status",
|
|
719
|
-
"query",
|
|
720
|
-
"url",
|
|
721
|
-
"final_url",
|
|
722
|
-
"title",
|
|
723
|
-
"description",
|
|
724
|
-
"domain_type",
|
|
725
|
-
"location",
|
|
726
|
-
"language",
|
|
727
|
-
"author",
|
|
728
|
-
"published_date",
|
|
729
|
-
"format",
|
|
730
|
-
"results_count",
|
|
731
|
-
"total_results",
|
|
732
|
-
"text_length",
|
|
733
|
-
"links_count",
|
|
734
|
-
"image_links_count",
|
|
735
|
-
"latency_ms",
|
|
736
|
-
"request_origin",
|
|
737
|
-
"id",
|
|
738
|
-
"request_id",
|
|
739
|
-
"error",
|
|
740
|
-
)
|
|
741
|
-
|
|
742
|
-
_USAGE_FIELD_LABELS = {
|
|
743
|
-
"id": "ID",
|
|
744
|
-
"url": "URL",
|
|
745
|
-
"final_url": "Final URL",
|
|
746
|
-
"latency_ms": "Latency (ms)",
|
|
747
|
-
"request_id": "Request ID",
|
|
748
|
-
}
|
|
749
|
-
|
|
705
|
+
def _wallet_amount(value: Any, currency: Any) -> str:
|
|
706
|
+
amount = str(value) if value is not None else "unavailable"
|
|
707
|
+
code = str(currency or "").strip()
|
|
708
|
+
if amount == "unavailable":
|
|
709
|
+
return amount
|
|
710
|
+
if code == "USD":
|
|
711
|
+
return f"${amount} USD"
|
|
712
|
+
return f"{amount} {code}".rstrip()
|
|
750
713
|
|
|
751
|
-
def _usage_field_label(key: str) -> str:
|
|
752
|
-
return _USAGE_FIELD_LABELS.get(key, key.replace("_", " ").capitalize())
|
|
753
714
|
|
|
715
|
+
def _wallet_text_lines(wallet: dict[str, Any]) -> list[str]:
|
|
716
|
+
currency = wallet.get("currency")
|
|
717
|
+
lines = [
|
|
718
|
+
"TinyFish usage",
|
|
719
|
+
f" Available balance: {_wallet_amount(wallet.get('available_balance'), currency)}",
|
|
720
|
+
f" As of: {wallet.get('as_of') or 'unavailable'}",
|
|
721
|
+
]
|
|
754
722
|
|
|
755
|
-
|
|
756
|
-
if
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
total_pages = data.get("total_pages")
|
|
785
|
-
if isinstance(page, int) and isinstance(total_pages, int):
|
|
786
|
-
parts.append(f"page {page} of {total_pages}")
|
|
787
|
-
limit = data.get("limit")
|
|
788
|
-
if isinstance(limit, int) and not isinstance(limit, bool):
|
|
789
|
-
parts.append(f"limit {limit}")
|
|
790
|
-
if isinstance(data.get("has_more"), bool):
|
|
791
|
-
parts.append(f"more: {'yes' if data['has_more'] else 'no'}")
|
|
792
|
-
return " | ".join(parts)
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
def _usage_text_lines(payload: dict[str, Any]) -> list[str]:
|
|
796
|
-
lines = ["TinyFish usage"]
|
|
797
|
-
surfaces = payload.get("surfaces")
|
|
798
|
-
if not isinstance(surfaces, dict):
|
|
799
|
-
return [*lines, " Invalid usage response."]
|
|
800
|
-
|
|
801
|
-
for surface_name in ("search", "fetch"):
|
|
802
|
-
lines.extend(("", surface_name.capitalize()))
|
|
803
|
-
result = surfaces.get(surface_name)
|
|
804
|
-
if not isinstance(result, dict):
|
|
805
|
-
lines.append(" No response.")
|
|
806
|
-
continue
|
|
807
|
-
if not result.get("success"):
|
|
808
|
-
lines.append(f" Error: {result.get('error') or 'Unknown error'}")
|
|
809
|
-
continue
|
|
810
|
-
|
|
811
|
-
data = result.get("data")
|
|
812
|
-
if not isinstance(data, dict):
|
|
813
|
-
lines.append(" Invalid usage response.")
|
|
814
|
-
continue
|
|
815
|
-
items = _usage_items(data)
|
|
816
|
-
lines.append(f" {_usage_summary(data, shown=len(items))}")
|
|
817
|
-
if not items:
|
|
818
|
-
lines.append(" No operations found on this page.")
|
|
819
|
-
continue
|
|
820
|
-
|
|
821
|
-
for index, item in enumerate(items, start=1):
|
|
822
|
-
lines.extend(("", f" Operation {index}"))
|
|
823
|
-
if not isinstance(item, dict):
|
|
824
|
-
lines.append(f" Value: {_usage_value_lines(item)[0]}")
|
|
723
|
+
auto_reload = wallet.get("auto_reload")
|
|
724
|
+
if auto_reload is None:
|
|
725
|
+
lines.append(" Auto-reload: unavailable")
|
|
726
|
+
elif isinstance(auto_reload, dict):
|
|
727
|
+
state = str(auto_reload.get("state") or "unknown").replace("_", " ")
|
|
728
|
+
if state == "unconfigured":
|
|
729
|
+
lines.append(" Auto-reload: not configured")
|
|
730
|
+
else:
|
|
731
|
+
threshold = _wallet_amount(auto_reload.get("threshold"), currency)
|
|
732
|
+
recharge_to = _wallet_amount(auto_reload.get("recharge_to"), currency)
|
|
733
|
+
lines.append(f" Auto-reload: {state} (at {threshold}, recharge to {recharge_to})")
|
|
734
|
+
else:
|
|
735
|
+
lines.append(" Auto-reload: invalid response")
|
|
736
|
+
|
|
737
|
+
pending = wallet.get("pending_top_up")
|
|
738
|
+
if pending is None:
|
|
739
|
+
lines.append(" Pending top-up: none")
|
|
740
|
+
elif isinstance(pending, dict):
|
|
741
|
+
amount = _wallet_amount(pending.get("amount"), currency)
|
|
742
|
+
lines.append(f" Pending top-up: {amount} since {pending.get('started_at') or 'unknown'}")
|
|
743
|
+
else:
|
|
744
|
+
lines.append(" Pending top-up: invalid response")
|
|
745
|
+
|
|
746
|
+
rates = wallet.get("rates")
|
|
747
|
+
meters = rates.get("meters") if isinstance(rates, dict) else None
|
|
748
|
+
if isinstance(meters, list) and meters:
|
|
749
|
+
lines.append(" Rates:")
|
|
750
|
+
for meter in meters:
|
|
751
|
+
if not isinstance(meter, dict):
|
|
825
752
|
continue
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
753
|
+
label = meter.get("label") or meter.get("product_id") or "Unnamed meter"
|
|
754
|
+
amount = _wallet_amount(meter.get("unit_amount"), meter.get("currency"))
|
|
755
|
+
per = f" per {meter['per']}" if meter.get("per") else ""
|
|
756
|
+
lines.append(f" {label}: {amount}{per}")
|
|
757
|
+
elif rates is None:
|
|
758
|
+
lines.append(" Rates: unavailable")
|
|
759
|
+
else:
|
|
760
|
+
lines.append(" Rates: none")
|
|
761
|
+
|
|
762
|
+
lines.append(" Historical spend: not provided by TinyFish's documented APIs")
|
|
832
763
|
return lines
|
|
833
764
|
|
|
834
765
|
|
|
@@ -836,16 +767,39 @@ def cmd_usage(args: argparse.Namespace) -> int:
|
|
|
836
767
|
api_key = _api_key_or_error()
|
|
837
768
|
if not api_key:
|
|
838
769
|
return 1
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
770
|
+
try:
|
|
771
|
+
wallet = rest_client.wallet(api_key=api_key)
|
|
772
|
+
except rest_client.TinyFishWalletNotFound:
|
|
773
|
+
payload = {
|
|
774
|
+
"success": True,
|
|
775
|
+
"wallet_available": False,
|
|
776
|
+
"reason": "legacy_billing_or_no_metronome_customer",
|
|
777
|
+
"message": "This account uses legacy billing or does not have a Metronome wallet yet.",
|
|
778
|
+
"billing_url": TINYFISH_BILLING_URL,
|
|
779
|
+
}
|
|
780
|
+
text_lines = [
|
|
781
|
+
"TinyFish usage",
|
|
782
|
+
" Wallet balance unavailable.",
|
|
783
|
+
f" {payload['message']}",
|
|
784
|
+
" Historical spend is not provided by TinyFish's documented APIs.",
|
|
785
|
+
f" Billing: {TINYFISH_BILLING_URL}",
|
|
786
|
+
]
|
|
787
|
+
except Exception as exc: # noqa: BLE001
|
|
788
|
+
payload = {"success": False, "wallet_available": None, "error": str(exc)}
|
|
789
|
+
text_lines = ["TinyFish usage", f" Error: {exc}"]
|
|
790
|
+
else:
|
|
791
|
+
if not isinstance(wallet, dict):
|
|
792
|
+
payload = {
|
|
793
|
+
"success": False,
|
|
794
|
+
"wallet_available": None,
|
|
795
|
+
"error": "TinyFish Wallet returned an invalid response",
|
|
796
|
+
}
|
|
797
|
+
text_lines = ["TinyFish usage", f" Error: {payload['error']}"]
|
|
798
|
+
else:
|
|
799
|
+
payload = {"success": True, "wallet_available": True, "wallet": wallet}
|
|
800
|
+
text_lines = _wallet_text_lines(wallet)
|
|
847
801
|
if bool(getattr(args, "json", False)):
|
|
848
802
|
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
849
803
|
else:
|
|
850
|
-
print("\n".join(
|
|
804
|
+
print("\n".join(text_lines))
|
|
851
805
|
return 0 if payload["success"] else 1
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
name: web-tinyfish
|
|
2
|
-
version: 0.3.
|
|
2
|
+
version: 0.3.4
|
|
3
3
|
description: "Independent TinyFish provider plugin for Hermes Agent. Adds MCP-first Search/Fetch, plain-language tool routing, update awareness, and optional default-deny Browser infrastructure."
|
|
4
4
|
author: gabeosx
|
|
5
5
|
kind: backend
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "hermes-plugin-tinyfish"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.4"
|
|
8
8
|
description = "TinyFish Search, Fetch, plain-language routing, update awareness, and optional Browser for Hermes Agent"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -183,16 +183,55 @@ def test_search_usage_uses_documented_search_endpoint(monkeypatch: pytest.Monkey
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
|
|
186
|
-
def
|
|
186
|
+
def test_wallet_uses_documented_endpoint(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
187
187
|
captured: dict[str, Any] = {}
|
|
188
188
|
|
|
189
|
-
def
|
|
189
|
+
def fake_get(url: str, **kwargs: Any) -> httpx.Response:
|
|
190
|
+
captured.update(url=url, **kwargs)
|
|
191
|
+
return _response("GET", url, payload={"available_balance": "12.34", "currency": "USD"})
|
|
192
|
+
|
|
193
|
+
monkeypatch.setattr(rest_client.httpx, "get", fake_get)
|
|
194
|
+
|
|
195
|
+
result = rest_client.wallet(api_key="tf_test", timeout=13.0)
|
|
196
|
+
|
|
197
|
+
assert result == {"available_balance": "12.34", "currency": "USD"}
|
|
198
|
+
assert captured == {
|
|
199
|
+
"url": "https://agent.tinyfish.ai/v1/wallet",
|
|
200
|
+
"headers": {"X-API-Key": "tf_test", "Accept": "application/json"},
|
|
201
|
+
"timeout": 13.0,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_wallet_reports_documented_not_found_account_state(
|
|
206
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
207
|
+
) -> None:
|
|
208
|
+
def fake_get(url: str, **kwargs: Any) -> httpx.Response:
|
|
209
|
+
return _response(
|
|
210
|
+
"GET",
|
|
211
|
+
url,
|
|
212
|
+
status=404,
|
|
213
|
+
payload={"error": {"code": "NOT_FOUND", "message": "Wallet not found"}},
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
monkeypatch.setattr(rest_client.httpx, "get", fake_get)
|
|
217
|
+
|
|
218
|
+
with pytest.raises(
|
|
219
|
+
rest_client.TinyFishWalletNotFound,
|
|
220
|
+
match="legacy billing or no Metronome customer yet",
|
|
221
|
+
):
|
|
222
|
+
rest_client.wallet(api_key="tf_test")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_usage_is_a_wallet_compatibility_wrapper(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
226
|
+
captured: dict[str, Any] = {}
|
|
227
|
+
|
|
228
|
+
def fake_wallet(*, api_key: str, timeout: float) -> dict[str, Any]:
|
|
190
229
|
captured.update(api_key=api_key, timeout=timeout)
|
|
191
|
-
return {"
|
|
230
|
+
return {"available_balance": "12.34"}
|
|
192
231
|
|
|
193
|
-
monkeypatch.setattr(rest_client, "
|
|
232
|
+
monkeypatch.setattr(rest_client, "wallet", fake_wallet)
|
|
194
233
|
|
|
195
|
-
assert rest_client.usage(api_key="tf_test", timeout=14.0) == {"
|
|
234
|
+
assert rest_client.usage(api_key="tf_test", timeout=14.0) == {"available_balance": "12.34"}
|
|
196
235
|
assert captured == {"api_key": "tf_test", "timeout": 14.0}
|
|
197
236
|
|
|
198
237
|
|
|
@@ -211,6 +250,7 @@ RestCall = Callable[[], dict[str, Any]]
|
|
|
211
250
|
),
|
|
212
251
|
("get", lambda: rest_client.search_usage(api_key="tf_test"), "TinyFish Search usage"),
|
|
213
252
|
("get", lambda: rest_client.fetch_usage(api_key="tf_test"), "TinyFish Fetch usage"),
|
|
253
|
+
("get", lambda: rest_client.wallet(api_key="tf_test"), "TinyFish Wallet"),
|
|
214
254
|
],
|
|
215
255
|
)
|
|
216
256
|
@pytest.mark.parametrize("status", [402, 500])
|
|
@@ -245,6 +285,7 @@ def test_rest_methods_report_http_failures(
|
|
|
245
285
|
),
|
|
246
286
|
("get", lambda: rest_client.search_usage(api_key="tf_test"), "TinyFish Search usage"),
|
|
247
287
|
("get", lambda: rest_client.fetch_usage(api_key="tf_test"), "TinyFish Fetch usage"),
|
|
288
|
+
("get", lambda: rest_client.wallet(api_key="tf_test"), "TinyFish Wallet"),
|
|
248
289
|
],
|
|
249
290
|
)
|
|
250
291
|
def test_rest_methods_report_transport_failures(
|
|
@@ -274,6 +315,7 @@ def test_rest_methods_report_transport_failures(
|
|
|
274
315
|
),
|
|
275
316
|
("get", lambda: rest_client.search_usage(api_key="tf_test"), "TinyFish Search usage"),
|
|
276
317
|
("get", lambda: rest_client.fetch_usage(api_key="tf_test"), "TinyFish Fetch usage"),
|
|
318
|
+
("get", lambda: rest_client.wallet(api_key="tf_test"), "TinyFish Wallet"),
|
|
277
319
|
],
|
|
278
320
|
)
|
|
279
321
|
def test_rest_methods_report_invalid_json(
|
|
@@ -715,86 +715,55 @@ def test_credits_reset_removes_retired_keys_and_leaves_browser_denied(
|
|
|
715
715
|
assert saved[-1]["tinyfish"]["credit_policy"] == {"browser": "deny"}
|
|
716
716
|
|
|
717
717
|
|
|
718
|
-
def
|
|
718
|
+
def test_usage_calls_wallet_and_returns_machine_readable_billing_data(
|
|
719
719
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
720
720
|
) -> None:
|
|
721
|
-
calls: list[
|
|
721
|
+
calls: list[str] = []
|
|
722
722
|
monkeypatch.setattr(cli, "_get_env", lambda name: "tf_secret")
|
|
723
723
|
|
|
724
|
-
def
|
|
725
|
-
calls.append(
|
|
726
|
-
return {"
|
|
724
|
+
def wallet(*, api_key: str) -> dict[str, Any]:
|
|
725
|
+
calls.append(api_key)
|
|
726
|
+
return {"available_balance": "21.44", "currency": "USD"}
|
|
727
727
|
|
|
728
|
-
|
|
729
|
-
calls.append(("fetch", api_key))
|
|
730
|
-
return {"operations": [{"status": "completed"}]}
|
|
731
|
-
|
|
732
|
-
monkeypatch.setattr(cli.rest_client, "search_usage", search_usage)
|
|
733
|
-
monkeypatch.setattr(cli.rest_client, "fetch_usage", fetch_usage)
|
|
728
|
+
monkeypatch.setattr(cli.rest_client, "wallet", wallet)
|
|
734
729
|
|
|
735
730
|
result = cli.cmd_usage(argparse.Namespace(json=True))
|
|
736
731
|
payload = json.loads(capsys.readouterr().out)
|
|
737
732
|
|
|
738
733
|
assert result == 0
|
|
739
|
-
assert calls == [
|
|
734
|
+
assert calls == ["tf_secret"]
|
|
740
735
|
assert payload == {
|
|
741
736
|
"success": True,
|
|
742
|
-
"
|
|
743
|
-
|
|
744
|
-
"success": True,
|
|
745
|
-
"data": {"items": [{"status": "completed"}]},
|
|
746
|
-
},
|
|
747
|
-
"fetch": {
|
|
748
|
-
"success": True,
|
|
749
|
-
"data": {"operations": [{"status": "completed"}]},
|
|
750
|
-
},
|
|
751
|
-
},
|
|
737
|
+
"wallet_available": True,
|
|
738
|
+
"wallet": {"available_balance": "21.44", "currency": "USD"},
|
|
752
739
|
}
|
|
753
740
|
assert "tf_secret" not in json.dumps(payload)
|
|
754
741
|
|
|
755
742
|
|
|
756
|
-
def
|
|
743
|
+
def test_usage_text_output_formats_wallet_for_people(
|
|
757
744
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
758
745
|
) -> None:
|
|
759
746
|
monkeypatch.setattr(cli, "_get_env", lambda name: "tf_secret")
|
|
760
747
|
monkeypatch.setattr(
|
|
761
748
|
cli.rest_client,
|
|
762
|
-
"
|
|
763
|
-
lambda *, api_key: {
|
|
764
|
-
"items": [
|
|
765
|
-
{
|
|
766
|
-
"id": "search-1",
|
|
767
|
-
"query": "Hermes Agent",
|
|
768
|
-
"status": "completed",
|
|
769
|
-
"results_count": 10,
|
|
770
|
-
"latency_ms": 125,
|
|
771
|
-
"created_at": "2026-08-12T20:00:00Z",
|
|
772
|
-
}
|
|
773
|
-
],
|
|
774
|
-
"total": 3,
|
|
775
|
-
"limit": 1,
|
|
776
|
-
"page": 1,
|
|
777
|
-
"total_pages": 3,
|
|
778
|
-
"has_more": True,
|
|
779
|
-
},
|
|
780
|
-
)
|
|
781
|
-
monkeypatch.setattr(
|
|
782
|
-
cli.rest_client,
|
|
783
|
-
"fetch_usage",
|
|
749
|
+
"wallet",
|
|
784
750
|
lambda *, api_key: {
|
|
785
|
-
"
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
751
|
+
"available_balance": "21.44",
|
|
752
|
+
"currency": "USD",
|
|
753
|
+
"as_of": "2026-08-13T12:00:00Z",
|
|
754
|
+
"auto_reload": {"state": "on", "threshold": "10.00", "recharge_to": "50.00"},
|
|
755
|
+
"pending_top_up": {"amount": "50.00", "started_at": "2026-08-13T11:59:00Z"},
|
|
756
|
+
"rates": {
|
|
757
|
+
"meters": [
|
|
758
|
+
{
|
|
759
|
+
"product_id": "prod_agent",
|
|
760
|
+
"label": "Agent steps",
|
|
761
|
+
"unit_amount": "0.016000",
|
|
762
|
+
"currency": "USD",
|
|
763
|
+
"per": "step",
|
|
764
|
+
}
|
|
765
|
+
]
|
|
766
|
+
},
|
|
798
767
|
},
|
|
799
768
|
)
|
|
800
769
|
|
|
@@ -805,92 +774,89 @@ def test_usage_text_output_formats_operation_history_for_people(
|
|
|
805
774
|
assert (
|
|
806
775
|
output
|
|
807
776
|
== """TinyFish usage
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
Query: Hermes Agent
|
|
816
|
-
Results count: 10
|
|
817
|
-
Latency (ms): 125
|
|
818
|
-
ID: search-1
|
|
819
|
-
|
|
820
|
-
Fetch
|
|
821
|
-
1 shown / 1 total | page 1 of 1 | limit 100 | more: no
|
|
822
|
-
|
|
823
|
-
Operation 1
|
|
824
|
-
Created at: 2026-08-12T20:01:00Z
|
|
825
|
-
Status: failed
|
|
826
|
-
URL: https://example.com
|
|
827
|
-
Error: timeout
|
|
777
|
+
Available balance: $21.44 USD
|
|
778
|
+
As of: 2026-08-13T12:00:00Z
|
|
779
|
+
Auto-reload: on (at $10.00 USD, recharge to $50.00 USD)
|
|
780
|
+
Pending top-up: $50.00 USD since 2026-08-13T11:59:00Z
|
|
781
|
+
Rates:
|
|
782
|
+
Agent steps: $0.016000 USD per step
|
|
783
|
+
Historical spend: not provided by TinyFish's documented APIs
|
|
828
784
|
"""
|
|
829
785
|
)
|
|
830
|
-
assert "{'" not in output
|
|
831
786
|
assert "tf_secret" not in output
|
|
787
|
+
assert "query" not in output.lower()
|
|
788
|
+
assert "url" not in output.lower()
|
|
832
789
|
|
|
833
790
|
|
|
834
|
-
def
|
|
791
|
+
def test_usage_wallet_not_found_explains_legacy_billing(
|
|
835
792
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
836
793
|
) -> None:
|
|
837
794
|
monkeypatch.setattr(cli, "_get_env", lambda name: "tf_secret")
|
|
838
|
-
monkeypatch.setattr(cli.rest_client, "search_usage", lambda *, api_key: {"items": []})
|
|
839
795
|
|
|
840
|
-
def
|
|
841
|
-
raise
|
|
796
|
+
def wallet_not_found(*, api_key: str) -> dict[str, Any]:
|
|
797
|
+
raise cli.rest_client.TinyFishWalletNotFound("wallet not found")
|
|
842
798
|
|
|
843
|
-
monkeypatch.setattr(cli.rest_client, "
|
|
799
|
+
monkeypatch.setattr(cli.rest_client, "wallet", wallet_not_found)
|
|
844
800
|
|
|
845
801
|
result = cli.cmd_usage(argparse.Namespace(json=False))
|
|
846
802
|
output = capsys.readouterr().out
|
|
847
803
|
|
|
848
|
-
assert result ==
|
|
849
|
-
assert "
|
|
850
|
-
assert "
|
|
804
|
+
assert result == 0
|
|
805
|
+
assert "Wallet balance unavailable" in output
|
|
806
|
+
assert "legacy billing or does not have a Metronome wallet" in output
|
|
807
|
+
assert "Historical spend is not provided" in output
|
|
808
|
+
assert cli.TINYFISH_BILLING_URL in output
|
|
851
809
|
|
|
852
810
|
|
|
853
|
-
def
|
|
811
|
+
def test_usage_wallet_not_found_is_machine_readable(
|
|
854
812
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
855
813
|
) -> None:
|
|
856
814
|
monkeypatch.setattr(cli, "_get_env", lambda name: "tf_secret")
|
|
857
|
-
monkeypatch.setattr(
|
|
858
|
-
cli.rest_client,
|
|
859
|
-
"search_usage",
|
|
860
|
-
lambda *, api_key: {"items": [{"status": "completed"}]},
|
|
861
|
-
)
|
|
862
815
|
|
|
863
|
-
def
|
|
864
|
-
raise
|
|
816
|
+
def wallet_not_found(*, api_key: str) -> dict[str, Any]:
|
|
817
|
+
raise cli.rest_client.TinyFishWalletNotFound("wallet not found")
|
|
818
|
+
|
|
819
|
+
monkeypatch.setattr(cli.rest_client, "wallet", wallet_not_found)
|
|
820
|
+
|
|
821
|
+
result = cli.cmd_usage(argparse.Namespace(json=True))
|
|
822
|
+
payload = json.loads(capsys.readouterr().out)
|
|
823
|
+
|
|
824
|
+
assert result == 0
|
|
825
|
+
assert payload == {
|
|
826
|
+
"success": True,
|
|
827
|
+
"wallet_available": False,
|
|
828
|
+
"reason": "legacy_billing_or_no_metronome_customer",
|
|
829
|
+
"message": "This account uses legacy billing or does not have a Metronome wallet yet.",
|
|
830
|
+
"billing_url": cli.TINYFISH_BILLING_URL,
|
|
831
|
+
}
|
|
832
|
+
|
|
865
833
|
|
|
866
|
-
|
|
834
|
+
def test_usage_transport_failure_is_reported_without_history(
|
|
835
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
836
|
+
) -> None:
|
|
837
|
+
monkeypatch.setattr(cli, "_get_env", lambda name: "tf_secret")
|
|
838
|
+
|
|
839
|
+
def fail_wallet(*, api_key: str) -> dict[str, Any]:
|
|
840
|
+
raise cli.rest_client.TinyFishRestError("Could not reach TinyFish Wallet")
|
|
841
|
+
|
|
842
|
+
monkeypatch.setattr(cli.rest_client, "wallet", fail_wallet)
|
|
867
843
|
|
|
868
844
|
result = cli.cmd_usage(argparse.Namespace(json=True))
|
|
869
845
|
payload = json.loads(capsys.readouterr().out)
|
|
870
846
|
|
|
871
847
|
assert result == 1
|
|
872
|
-
assert payload
|
|
873
|
-
assert payload["surfaces"]["search"]["success"] is True
|
|
874
|
-
assert payload["surfaces"]["fetch"] == {
|
|
848
|
+
assert payload == {
|
|
875
849
|
"success": False,
|
|
876
|
-
"
|
|
850
|
+
"wallet_available": None,
|
|
851
|
+
"error": "Could not reach TinyFish Wallet",
|
|
877
852
|
}
|
|
878
853
|
|
|
879
854
|
|
|
880
|
-
def
|
|
855
|
+
def test_usage_without_api_key_fails_before_calling_wallet(
|
|
881
856
|
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
882
857
|
) -> None:
|
|
883
858
|
monkeypatch.setattr(cli, "_get_env", lambda name: "")
|
|
884
|
-
monkeypatch.setattr(
|
|
885
|
-
cli.rest_client,
|
|
886
|
-
"search_usage",
|
|
887
|
-
lambda **kwargs: pytest.fail("Search usage must not be called"),
|
|
888
|
-
)
|
|
889
|
-
monkeypatch.setattr(
|
|
890
|
-
cli.rest_client,
|
|
891
|
-
"fetch_usage",
|
|
892
|
-
lambda **kwargs: pytest.fail("Fetch usage must not be called"),
|
|
893
|
-
)
|
|
859
|
+
monkeypatch.setattr(cli.rest_client, "wallet", lambda **kwargs: pytest.fail("Wallet must not be called"))
|
|
894
860
|
|
|
895
861
|
result = cli.cmd_usage(argparse.Namespace(json=True))
|
|
896
862
|
captured = capsys.readouterr()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/health.py
RENAMED
|
File without changes
|
{hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/normalize.py
RENAMED
|
File without changes
|
{hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/provider.py
RENAMED
|
File without changes
|
|
File without changes
|
{hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/hermes_plugin_tinyfish/update_check.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_browser_provider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hermes_plugin_tinyfish-0.3.3 → hermes_plugin_tinyfish-0.3.4}/tests/test_validate_release.py
RENAMED
|
File without changes
|
|
File without changes
|