web2cli 0.2.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.
Files changed (44) hide show
  1. web2cli/__init__.py +3 -0
  2. web2cli/__main__.py +5 -0
  3. web2cli/adapter/__init__.py +0 -0
  4. web2cli/adapter/lint.py +667 -0
  5. web2cli/adapter/loader.py +157 -0
  6. web2cli/adapter/validator.py +127 -0
  7. web2cli/adapters/discord.com/web2cli.yaml +476 -0
  8. web2cli/adapters/mail.google.com/parsers/inbox.py +200 -0
  9. web2cli/adapters/mail.google.com/web2cli.yaml +52 -0
  10. web2cli/adapters/news.ycombinator.com/web2cli.yaml +356 -0
  11. web2cli/adapters/reddit.com/web2cli.yaml +233 -0
  12. web2cli/adapters/slack.com/web2cli.yaml +445 -0
  13. web2cli/adapters/stackoverflow.com/web2cli.yaml +257 -0
  14. web2cli/adapters/x.com/providers/x_graphql.py +299 -0
  15. web2cli/adapters/x.com/web2cli.yaml +449 -0
  16. web2cli/auth/__init__.py +0 -0
  17. web2cli/auth/browser_login.py +820 -0
  18. web2cli/auth/manager.py +166 -0
  19. web2cli/auth/store.py +68 -0
  20. web2cli/cli.py +1286 -0
  21. web2cli/executor/__init__.py +0 -0
  22. web2cli/executor/http.py +113 -0
  23. web2cli/output/__init__.py +0 -0
  24. web2cli/output/formatter.py +116 -0
  25. web2cli/parser/__init__.py +0 -0
  26. web2cli/parser/custom.py +21 -0
  27. web2cli/parser/html_parser.py +111 -0
  28. web2cli/parser/transforms.py +127 -0
  29. web2cli/pipe.py +10 -0
  30. web2cli/providers/__init__.py +6 -0
  31. web2cli/providers/base.py +22 -0
  32. web2cli/providers/registry.py +86 -0
  33. web2cli/runtime/__init__.py +1 -0
  34. web2cli/runtime/cache.py +42 -0
  35. web2cli/runtime/engine.py +743 -0
  36. web2cli/runtime/parser.py +398 -0
  37. web2cli/runtime/template.py +52 -0
  38. web2cli/types.py +71 -0
  39. web2cli-0.2.0.dist-info/METADATA +467 -0
  40. web2cli-0.2.0.dist-info/RECORD +44 -0
  41. web2cli-0.2.0.dist-info/WHEEL +5 -0
  42. web2cli-0.2.0.dist-info/entry_points.txt +2 -0
  43. web2cli-0.2.0.dist-info/licenses/LICENSE +202 -0
  44. web2cli-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,166 @@
1
+ """Session management — create, get, remove, check.
2
+
3
+ Handles cookie parsing, env var fallback, and persistence via store.
4
+ """
5
+
6
+ import json
7
+ import os
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+
11
+ from web2cli.auth.store import (
12
+ delete_session,
13
+ load_session,
14
+ save_session,
15
+ session_exists,
16
+ )
17
+ from web2cli.types import Session
18
+
19
+
20
+ def parse_cookie_string(raw: str) -> dict[str, str]:
21
+ """Parse "k=v; k2=v2" into dict."""
22
+ cookies = {}
23
+ for pair in raw.split(";"):
24
+ pair = pair.strip()
25
+ if "=" in pair:
26
+ key, _, value = pair.partition("=")
27
+ cookies[key.strip()] = value.strip()
28
+ return cookies
29
+
30
+
31
+ def parse_cookie_file(path: str) -> dict[str, str]:
32
+ """Load a JSON file as cookie dict."""
33
+ with open(path) as f:
34
+ data = json.load(f)
35
+ if not isinstance(data, dict):
36
+ raise ValueError(f"Cookie file must contain a JSON object, got {type(data).__name__}")
37
+ return {str(k): str(v) for k, v in data.items()}
38
+
39
+
40
+ def create_session(
41
+ domain: str,
42
+ cookies: dict[str, str] | None = None,
43
+ token: str | None = None,
44
+ ) -> Session:
45
+ """Create and persist a session."""
46
+ now = datetime.now(timezone.utc).isoformat()
47
+
48
+ if cookies and token:
49
+ auth_type = "cookies+token"
50
+ data = {"cookies": cookies, "token": token}
51
+ elif cookies:
52
+ auth_type = "cookies"
53
+ data = {"cookies": cookies}
54
+ elif token:
55
+ auth_type = "token"
56
+ data = {"token": token}
57
+ else:
58
+ raise ValueError("Either cookies or token must be provided")
59
+
60
+ session = Session(
61
+ domain=domain,
62
+ auth_type=auth_type,
63
+ data=data,
64
+ created_at=now,
65
+ last_used=now,
66
+ )
67
+
68
+ save_session(domain, {
69
+ "domain": session.domain,
70
+ "auth_type": session.auth_type,
71
+ "data": session.data,
72
+ "created_at": session.created_at,
73
+ "last_used": session.last_used,
74
+ })
75
+
76
+ return session
77
+
78
+
79
+ def get_session(domain: str, auth_spec: dict | None = None) -> Session | None:
80
+ """Retrieve session for a domain.
81
+
82
+ Checks env var first (from auth_spec), then stored session file.
83
+ """
84
+ # Env var fallback
85
+ if auth_spec:
86
+ env_cookies: dict[str, str] = {}
87
+ env_token: str | None = None
88
+ for method in auth_spec.get("methods", []):
89
+ env_var = method.get("env_var")
90
+ if env_var:
91
+ env_val = os.environ.get(env_var)
92
+ if env_val:
93
+ method_type = str(method.get("type", "cookies")).lower()
94
+
95
+ if method_type == "token":
96
+ token = env_val.strip()
97
+ if token:
98
+ env_token = token
99
+ continue
100
+
101
+ # Default to cookies when method type is omitted.
102
+ cookies = parse_cookie_string(env_val)
103
+ env_cookies.update(cookies)
104
+
105
+ if env_cookies and env_token:
106
+ return Session(
107
+ domain=domain,
108
+ auth_type="cookies+token",
109
+ data={"cookies": env_cookies, "token": env_token},
110
+ )
111
+ if env_cookies:
112
+ return Session(
113
+ domain=domain,
114
+ auth_type="cookies",
115
+ data={"cookies": env_cookies},
116
+ )
117
+ if env_token:
118
+ return Session(
119
+ domain=domain,
120
+ auth_type="token",
121
+ data={"token": env_token},
122
+ )
123
+
124
+ # Stored session
125
+ raw = load_session(domain)
126
+ if raw is None:
127
+ return None
128
+
129
+ return Session(
130
+ domain=raw.get("domain", domain),
131
+ auth_type=raw.get("auth_type", "cookies"),
132
+ data=raw.get("data", {}),
133
+ created_at=raw.get("created_at", ""),
134
+ last_used=raw.get("last_used", ""),
135
+ )
136
+
137
+
138
+ def remove_session(domain: str) -> bool:
139
+ """Remove stored session."""
140
+ return delete_session(domain)
141
+
142
+
143
+ def check_session(domain: str) -> dict:
144
+ """Return session metadata (no secrets). For --status display."""
145
+ if not session_exists(domain):
146
+ return {"exists": False}
147
+
148
+ raw = load_session(domain)
149
+ if raw is None:
150
+ return {"exists": False, "error": "corrupt or unreadable"}
151
+
152
+ info = {
153
+ "exists": True,
154
+ "auth_type": raw.get("auth_type", "unknown"),
155
+ "created_at": raw.get("created_at", ""),
156
+ "last_used": raw.get("last_used", ""),
157
+ }
158
+
159
+ # Show cookie key names (not values) for verification
160
+ data = raw.get("data", {})
161
+ if "cookies" in data:
162
+ info["cookie_keys"] = list(data["cookies"].keys())
163
+ if "token" in data:
164
+ info["has_token"] = True
165
+
166
+ return info
web2cli/auth/store.py ADDED
@@ -0,0 +1,68 @@
1
+ """Encrypted session storage.
2
+
3
+ Sessions are stored at ~/.web2cli/sessions/<domain>.json.enc
4
+ using Fernet symmetric encryption derived from machine identity.
5
+ """
6
+
7
+ import base64
8
+ import hashlib
9
+ import json
10
+ import os
11
+ from pathlib import Path
12
+
13
+ from cryptography.fernet import Fernet, InvalidToken
14
+
15
+ SESSIONS_DIR = Path.home() / ".web2cli" / "sessions"
16
+ FIXED_SALT = b"web2cli-session-store-v1"
17
+
18
+
19
+ def _get_encryption_key() -> bytes:
20
+ """Derive a Fernet key from hostname + username + fixed string.
21
+
22
+ This is NOT a high-security secret vault — it's obfuscation
23
+ to prevent casual reading of cookie values on disk.
24
+ """
25
+ identity = f"{os.uname().nodename}:{os.getlogin()}:web2cli"
26
+ dk = hashlib.pbkdf2_hmac("sha256", identity.encode(), FIXED_SALT, 100_000)
27
+ return base64.urlsafe_b64encode(dk)
28
+
29
+
30
+ def _ensure_dir() -> None:
31
+ SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
32
+
33
+
34
+ def save_session(domain: str, data: dict) -> None:
35
+ """Encrypt and persist session data for a domain."""
36
+ _ensure_dir()
37
+ fernet = Fernet(_get_encryption_key())
38
+ encrypted = fernet.encrypt(json.dumps(data).encode())
39
+ path = SESSIONS_DIR / f"{domain}.json.enc"
40
+ path.write_bytes(encrypted)
41
+ path.chmod(0o600)
42
+
43
+
44
+ def load_session(domain: str) -> dict | None:
45
+ """Load and decrypt session for a domain. Returns None if missing/corrupt."""
46
+ path = SESSIONS_DIR / f"{domain}.json.enc"
47
+ if not path.exists():
48
+ return None
49
+ try:
50
+ fernet = Fernet(_get_encryption_key())
51
+ decrypted = fernet.decrypt(path.read_bytes())
52
+ return json.loads(decrypted)
53
+ except (InvalidToken, json.JSONDecodeError, OSError):
54
+ return None
55
+
56
+
57
+ def delete_session(domain: str) -> bool:
58
+ """Remove session file. Returns True if deleted."""
59
+ path = SESSIONS_DIR / f"{domain}.json.enc"
60
+ if path.exists():
61
+ path.unlink()
62
+ return True
63
+ return False
64
+
65
+
66
+ def session_exists(domain: str) -> bool:
67
+ """Check if a session file exists for a domain."""
68
+ return (SESSIONS_DIR / f"{domain}.json.enc").exists()