walla-cli 0.1.1__tar.gz → 0.1.3__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.
Files changed (77) hide show
  1. {walla_cli-0.1.1 → walla_cli-0.1.3}/PKG-INFO +1 -1
  2. {walla_cli-0.1.1 → walla_cli-0.1.3}/pyproject.toml +1 -1
  3. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_session_store.py +20 -0
  4. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/__init__.py +1 -1
  5. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/account/cookies.py +39 -7
  6. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/account/login.py +19 -4
  7. walla_cli-0.1.3/walla/account/prompt.py +84 -0
  8. walla_cli-0.1.3/walla/core/human_fix.py +31 -0
  9. walla_cli-0.1.1/walla/account/prompt.py +0 -56
  10. walla_cli-0.1.1/walla/core/human_fix.py +0 -26
  11. {walla_cli-0.1.1 → walla_cli-0.1.3}/.cursor/rules/quality.mdc +0 -0
  12. {walla_cli-0.1.1 → walla_cli-0.1.3}/.github/workflows/ci.yml +0 -0
  13. {walla_cli-0.1.1 → walla_cli-0.1.3}/.gitignore +0 -0
  14. {walla_cli-0.1.1 → walla_cli-0.1.3}/AGENTS.md +0 -0
  15. {walla_cli-0.1.1 → walla_cli-0.1.3}/LICENSE +0 -0
  16. {walla_cli-0.1.1 → walla_cli-0.1.3}/README.md +0 -0
  17. {walla_cli-0.1.1 → walla_cli-0.1.3}/docs/WIRE.md +0 -0
  18. {walla_cli-0.1.1 → walla_cli-0.1.3}/docs/assets/walla-github-banner.jpg +0 -0
  19. {walla_cli-0.1.1 → walla_cli-0.1.3}/docs/code_quality.md +0 -0
  20. {walla_cli-0.1.1 → walla_cli-0.1.3}/docs/data_engineering_standards.md +0 -0
  21. {walla_cli-0.1.1 → walla_cli-0.1.3}/docs/mcp.md +0 -0
  22. {walla_cli-0.1.1 → walla_cli-0.1.3}/docs/objective_function.md +0 -0
  23. {walla_cli-0.1.1 → walla_cli-0.1.3}/docs/rate_limit_analysis.md +0 -0
  24. {walla_cli-0.1.1 → walla_cli-0.1.3}/fixtures/categories.json +0 -0
  25. {walla_cli-0.1.1 → walla_cli-0.1.3}/fixtures/item_detail.json +0 -0
  26. {walla_cli-0.1.1 → walla_cli-0.1.3}/fixtures/search_bicicleta.json +0 -0
  27. {walla_cli-0.1.1 → walla_cli-0.1.3}/scripts/check_code_quality.py +0 -0
  28. {walla_cli-0.1.1 → walla_cli-0.1.3}/scripts/probe_rate.py +0 -0
  29. {walla_cli-0.1.1 → walla_cli-0.1.3}/skills/walla/SKILL.md +0 -0
  30. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_account.py +0 -0
  31. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_account_more.py +0 -0
  32. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_catch.py +0 -0
  33. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_cli_json.py +0 -0
  34. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_core.py +0 -0
  35. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_edges.py +0 -0
  36. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_http_more.py +0 -0
  37. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_live.py +0 -0
  38. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_parsers.py +0 -0
  39. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_polite.py +0 -0
  40. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_search_api.py +0 -0
  41. {walla_cli-0.1.1 → walla_cli-0.1.3}/tests/test_verdict.py +0 -0
  42. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/account/__init__.py +0 -0
  43. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/account/actions.py +0 -0
  44. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/account/inbox.py +0 -0
  45. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/account/session_store.py +0 -0
  46. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/cli/__init__.py +0 -0
  47. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/cli/banner.py +0 -0
  48. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/cli/catch.py +0 -0
  49. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/cli/cmd_account.py +0 -0
  50. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/cli/cmd_look.py +0 -0
  51. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/cli/cmd_ops.py +0 -0
  52. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/cli/console.py +0 -0
  53. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/cli/main.py +0 -0
  54. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/core/__init__.py +0 -0
  55. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/core/envelope.py +0 -0
  56. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/core/errors.py +0 -0
  57. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/core/exceptions.py +0 -0
  58. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/core/instruct.py +0 -0
  59. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/core/path.py +0 -0
  60. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/http/__init__.py +0 -0
  61. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/http/client.py +0 -0
  62. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/http/headers.py +0 -0
  63. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/http/polite.py +0 -0
  64. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/hunter/__init__.py +0 -0
  65. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/hunter/profile_store.py +0 -0
  66. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/hunter/verdict.py +0 -0
  67. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/hunter/watches.py +0 -0
  68. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/mcp/__init__.py +0 -0
  69. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/mcp/_entry.py +0 -0
  70. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/mcp/server.py +0 -0
  71. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/models/__init__.py +0 -0
  72. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/models/account.py +0 -0
  73. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/models/listing.py +0 -0
  74. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/models/profile.py +0 -0
  75. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/search/__init__.py +0 -0
  76. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/search/api.py +0 -0
  77. {walla_cli-0.1.1 → walla_cli-0.1.3}/walla/search/parse.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: walla-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Wallapop.es CLI - look · offer · talk for humans and agents
5
5
  Project-URL: Homepage, https://github.com/berendgort/wallapop-cli
6
6
  Project-URL: Repository, https://github.com/berendgort/wallapop-cli
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "walla-cli"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  description = "Wallapop.es CLI - look · offer · talk for humans and agents"
5
5
  authors = [{ name = "Dr. Berend Gort", email = "berend.gort@gmail.com" }]
6
6
  readme = "README.md"
@@ -53,3 +53,23 @@ def test_parse_cookie_missing(tmp_path: Path) -> None:
53
53
  f.write_text("foo=bar\n", encoding="utf-8")
54
54
  with pytest.raises(WallaAuthError):
55
55
  parse_cookie_export(f)
56
+
57
+
58
+ def test_parse_cookie_editor_name_colon_quotes() -> None:
59
+ from walla.account.cookies import parse_cookie_text
60
+
61
+ raw = '__Secure-next-auth.session-token:"eyJhbGci.aaa.bbb"'
62
+ cookie, _ = parse_cookie_text(raw)
63
+ assert cookie == "eyJhbGci.aaa.bbb"
64
+
65
+
66
+ def test_login_cookie_text_does_not_treat_blob_as_path() -> None:
67
+ from unittest.mock import patch
68
+
69
+ from walla.account.login import login_cookie_text
70
+
71
+ blob = '__Secure-next-auth.session-token:"' + ("x." * 400) + 'yyy"'
72
+ with patch("walla.account.login._session_from_cookie") as mock_sess:
73
+ mock_sess.return_value = SessionData(access_token="t", session_cookie="c")
74
+ login_cookie_text(blob)
75
+ assert mock_sess.call_args[0][0].startswith("x.")
@@ -4,4 +4,4 @@ from __future__ import annotations
4
4
 
5
5
  __all__ = ("__version__",)
6
6
 
7
- __version__ = "0.1.1"
7
+ __version__ = "0.1.3"
@@ -64,25 +64,57 @@ def _from_text(text: str) -> tuple[str, str | None]:
64
64
  if len(fields) >= 7:
65
65
  name, value = fields[5], fields[6]
66
66
  if name == SESSION_COOKIE:
67
- session = value
67
+ session = _strip_quotes(value)
68
68
  elif name == "device_id":
69
- device = value
69
+ device = _strip_quotes(value)
70
70
  continue
71
71
  line = line.removeprefix("Cookie:").strip()
72
+ # Cookie-Editor sometimes copies: name:"value" or name=value
73
+ if SESSION_COOKIE in line and (":" in line or "=" in line):
74
+ extracted = _extract_named(line, SESSION_COOKIE)
75
+ if extracted:
76
+ session = extracted
77
+ continue
72
78
  for pair in line.split(";"):
73
79
  pair = pair.strip()
74
- if "=" not in pair:
80
+ if "=" not in pair and ":" not in pair:
75
81
  if pair.count(".") >= 2 and len(pair) > 100:
76
- session = pair
82
+ session = _strip_quotes(pair)
77
83
  continue
78
- name, _, value = pair.partition("=")
79
- name, value = name.strip(), value.strip()
84
+ name, value = _split_pair(pair)
80
85
  if name == SESSION_COOKIE:
81
86
  session = value
82
87
  elif name == "device_id":
83
88
  device = value
84
89
  if not session and text.count(".") >= 2 and len(text.strip()) > 100:
85
- session = text.strip()
90
+ session = _strip_quotes(text.strip())
86
91
  if not session:
87
92
  raise WallaAuthError(f"no {SESSION_COOKIE} in cookie paste")
88
93
  return session, device
94
+
95
+
96
+ def _strip_quotes(value: str) -> str:
97
+ v = value.strip()
98
+ if len(v) >= 2 and v[0] == v[-1] and v[0] in "'\"":
99
+ return v[1:-1]
100
+ return v
101
+
102
+
103
+ def _split_pair(pair: str) -> tuple[str, str]:
104
+ if "=" in pair:
105
+ name, _, value = pair.partition("=")
106
+ else:
107
+ name, _, value = pair.partition(":")
108
+ return name.strip(), _strip_quotes(value.strip())
109
+
110
+
111
+ def _extract_named(line: str, cookie_name: str) -> str | None:
112
+ for sep in (":", "="):
113
+ prefix = f"{cookie_name}{sep}"
114
+ if line.startswith(prefix):
115
+ return _strip_quotes(line[len(prefix) :].strip())
116
+ # also tolerate leading junk
117
+ idx = line.find(prefix)
118
+ if idx >= 0:
119
+ return _strip_quotes(line[idx + len(prefix) :].strip())
120
+ return None
@@ -28,13 +28,28 @@ __all__ = (
28
28
 
29
29
  def login_cookie_text(text: str) -> SessionData:
30
30
  """Primary path: pasted cookie value, Cookie header, or export body."""
31
- path_candidate = Path(text.strip()).expanduser()
32
- if path_candidate.is_file():
33
- return login_cookies(path_candidate)
34
- cookie, device = parse_cookie_text(text)
31
+ stripped = text.strip()
32
+ if _looks_like_cookie_file_path(stripped):
33
+ return login_cookies(Path(stripped).expanduser())
34
+ cookie, device = parse_cookie_text(stripped)
35
35
  return _session_from_cookie(cookie, device)
36
36
 
37
37
 
38
+ def _looks_like_cookie_file_path(text: str) -> bool:
39
+ """True only for short path-like pastes; never treat JWT blobs as paths."""
40
+ if "\n" in text or len(text) > 512:
41
+ return False
42
+ if text.count(".") >= 2 and len(text) > 80 and "/" not in text and "\\" not in text:
43
+ return False
44
+ if SESSION_COOKIE in text or text.startswith("eyJ") or ':"' in text:
45
+ return False
46
+ path = Path(text).expanduser()
47
+ try:
48
+ return path.is_file()
49
+ except OSError:
50
+ return False
51
+
52
+
38
53
  def login_cookies(path: Path) -> SessionData:
39
54
  cookie, device = parse_cookie_export(path)
40
55
  return _session_from_cookie(cookie, device)
@@ -0,0 +1,84 @@
1
+ """Interactive login helpers (cookie paste primary)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import getpass
6
+ from typing import Any
7
+
8
+ import typer
9
+
10
+ from walla.account.cookies import SESSION_COOKIE
11
+ from walla.account.login import login_cookie_text, login_password
12
+ from walla.core.exceptions import WallaAuthError
13
+ from walla.models.account import SessionData
14
+
15
+ __all__ = ("interactive_login", "session_summary")
16
+
17
+ _COOKIE_HOWTO = f"""
18
+ How to get your Wallapop session cookie
19
+ =======================================
20
+
21
+ 1. Open Chrome/Firefox and go to:
22
+ https://es.wallapop.com
23
+ 2. Log in with your Wallapop account (until you see your feed).
24
+ 3. Copy the cookie named exactly:
25
+ {SESSION_COOKIE}
26
+
27
+ Easiest (extension):
28
+ - Install "Cookie-Editor" from the Chrome/Firefox store
29
+ - Click the Cookie-Editor icon on es.wallapop.com
30
+ - Find {SESSION_COOKIE}
31
+ - Click Copy on its Value
32
+
33
+ Or DevTools (no extension):
34
+ - Press F12 -> Application (Chrome) or Storage (Firefox)
35
+ - Cookies -> https://es.wallapop.com
36
+ - Click {SESSION_COOKIE}
37
+ - Double-click the Value cell -> Ctrl+C / Cmd+C
38
+
39
+ 4. Paste that long string below (one line), then press Enter.
40
+ Paste ONLY the Value (starts with eyJ...), or Cookie-Editor's
41
+ name:"value" form is also OK.
42
+ Do NOT paste your password. Do NOT share this cookie in chat.
43
+
44
+ Optional: save an export as cookies.txt and paste the file path instead.
45
+ """.strip()
46
+
47
+
48
+ def interactive_login(*, password: bool = False) -> SessionData:
49
+ """Prompt for cookie paste, or email/password when ``password`` is True."""
50
+ if password:
51
+ return _prompt_password()
52
+ return _prompt_cookie()
53
+
54
+
55
+ def _prompt_cookie() -> SessionData:
56
+ typer.echo(_COOKIE_HOWTO)
57
+ typer.echo("")
58
+ raw = typer.prompt(f"Paste {SESSION_COOKIE} value (or cookies.txt path)").strip()
59
+ if not raw:
60
+ raise WallaAuthError("empty paste - copy the cookie Value, not the name")
61
+ return login_cookie_text(raw)
62
+
63
+
64
+ def _prompt_password() -> SessionData:
65
+ typer.echo(
66
+ "Password login often fails (MFA). Prefer plain: walla login (paste cookie)."
67
+ )
68
+ email = typer.prompt("email").strip()
69
+ pw = getpass.getpass("password: ")
70
+ try:
71
+ return login_password(username=email, password=pw)
72
+ except WallaAuthError:
73
+ typer.echo("Password rejected. Falling back to cookie paste.")
74
+ return _prompt_cookie()
75
+
76
+
77
+ def session_summary(sess: SessionData) -> dict[str, Any]:
78
+ return {
79
+ "authenticated": True,
80
+ "has_access_token": bool(sess.access_token),
81
+ "has_cookie": bool(sess.session_cookie),
82
+ "user_id": sess.user_id,
83
+ "micro_name": sess.micro_name,
84
+ }
@@ -0,0 +1,31 @@
1
+ """User-visible fix recipes (not system prompts)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ __all__ = (
8
+ "cookie_export_fix",
9
+ )
10
+
11
+ _SESSION_COOKIE = "__Secure-next-auth.session-token"
12
+
13
+
14
+ def cookie_export_fix() -> dict[str, Any]:
15
+ return {
16
+ "title": "Paste Wallapop session cookie",
17
+ "steps": [
18
+ "Open https://es.wallapop.com and log in until you see your feed.",
19
+ f"Copy cookie {_SESSION_COOKIE} (Cookie-Editor extension, or F12 -> "
20
+ "Application/Storage -> Cookies -> es.wallapop.com -> copy Value).",
21
+ "Run: walla login",
22
+ "Paste the cookie Value (the long string), not the cookie name, then Enter.",
23
+ "Agents: walla login --cookie '<value>' --json",
24
+ "Never paste the cookie into a chat log.",
25
+ ],
26
+ "say_to_user": (
27
+ f"Run `walla login`, then paste the Value of cookie "
28
+ f"`{_SESSION_COOKIE}` from https://es.wallapop.com "
29
+ "(Cookie-Editor, or DevTools -> Application -> Cookies)."
30
+ ),
31
+ }
@@ -1,56 +0,0 @@
1
- """Interactive login helpers (cookie paste primary)."""
2
-
3
- from __future__ import annotations
4
-
5
- import getpass
6
- from typing import Any
7
-
8
- import typer
9
-
10
- from walla.account.cookies import SESSION_COOKIE
11
- from walla.account.login import login_cookie_text, login_password
12
- from walla.core.exceptions import WallaAuthError
13
- from walla.models.account import SessionData
14
-
15
- __all__ = ("interactive_login",)
16
-
17
-
18
- def interactive_login(*, password: bool = False) -> SessionData:
19
- """Prompt for cookie paste, or email/password when ``password`` is True."""
20
- if password:
21
- return _prompt_password()
22
- return _prompt_cookie()
23
-
24
-
25
- def _prompt_cookie() -> SessionData:
26
- typer.echo(
27
- f"Paste {SESSION_COOKIE} (or a cookies.txt path), then Enter:\n"
28
- " Tip: log in at es.wallapop.com → Cookie-Editor → copy that cookie."
29
- )
30
- raw = typer.prompt("cookie").strip()
31
- if not raw:
32
- raise WallaAuthError("empty paste")
33
- return login_cookie_text(raw)
34
-
35
-
36
- def _prompt_password() -> SessionData:
37
- typer.echo(
38
- "Password login often fails (MFA). Prefer: walla login (paste cookie)."
39
- )
40
- email = typer.prompt("email").strip()
41
- pw = getpass.getpass("password: ")
42
- try:
43
- return login_password(username=email, password=pw)
44
- except WallaAuthError:
45
- typer.echo("Password rejected. Paste the session cookie instead.")
46
- return _prompt_cookie()
47
-
48
-
49
- def session_summary(sess: SessionData) -> dict[str, Any]:
50
- return {
51
- "authenticated": True,
52
- "has_access_token": bool(sess.access_token),
53
- "has_cookie": bool(sess.session_cookie),
54
- "user_id": sess.user_id,
55
- "micro_name": sess.micro_name,
56
- }
@@ -1,26 +0,0 @@
1
- """User-visible fix recipes (not system prompts)."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import Any
6
-
7
- __all__ = (
8
- "cookie_export_fix",
9
- )
10
-
11
-
12
- def cookie_export_fix() -> dict[str, Any]:
13
- return {
14
- "title": "Paste Wallapop session cookie",
15
- "steps": [
16
- "Log in at https://es.wallapop.com in your browser.",
17
- "Copy cookie __Secure-next-auth.session-token (Cookie-Editor).",
18
- "Run: walla login",
19
- "Paste the cookie value when prompted.",
20
- "Agents: walla login --cookie '<value>' --json",
21
- ],
22
- "say_to_user": (
23
- "Run `walla login` and paste your "
24
- "__Secure-next-auth.session-token cookie from es.wallapop.com."
25
- ),
26
- }
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
File without changes
File without changes