scrapeMM 0.6.2__tar.gz → 0.6.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 (50) hide show
  1. {scrapemm-0.6.2/scrapeMM.egg-info → scrapemm-0.6.3}/PKG-INFO +1 -1
  2. {scrapemm-0.6.2 → scrapemm-0.6.3}/pyproject.toml +1 -1
  3. {scrapemm-0.6.2 → scrapemm-0.6.3}/requirements.txt +2 -1
  4. {scrapemm-0.6.2 → scrapemm-0.6.3/scrapeMM.egg-info}/PKG-INFO +1 -1
  5. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/decodo/decodo.py +13 -18
  6. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/retrieval.py +4 -2
  7. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/secrets.py +6 -8
  8. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/util.py +28 -6
  9. scrapemm-0.6.3/scripts/example.py +11 -0
  10. scrapemm-0.6.2/scripts/example.py +0 -10
  11. {scrapemm-0.6.2 → scrapemm-0.6.3}/.gitignore +0 -0
  12. {scrapemm-0.6.2 → scrapemm-0.6.3}/LICENSE +0 -0
  13. {scrapemm-0.6.2 → scrapemm-0.6.3}/README.md +0 -0
  14. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapeMM.egg-info/SOURCES.txt +0 -0
  15. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapeMM.egg-info/dependency_links.txt +0 -0
  16. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapeMM.egg-info/requires.txt +0 -0
  17. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapeMM.egg-info/top_level.txt +0 -0
  18. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/__init__.py +0 -0
  19. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/common/__init__.py +0 -0
  20. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/common/exceptions.py +0 -0
  21. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/common/scraping_response.py +0 -0
  22. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/__init__.py +0 -0
  23. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/common.py +0 -0
  24. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/images.py +0 -0
  25. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/media.py +0 -0
  26. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/requests.py +0 -0
  27. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/util.py +0 -0
  28. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/download/videos.py +0 -0
  29. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/__init__.py +0 -0
  30. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/archive_org.py +0 -0
  31. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/archive_today.py +0 -0
  32. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/base.py +0 -0
  33. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/bluesky.py +0 -0
  34. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/decodo/__init__.py +0 -0
  35. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/decodo/html_postprocessor.py +0 -0
  36. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/fb.py +0 -0
  37. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/firecrawl/__init__.py +0 -0
  38. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/firecrawl/firecrawl.py +0 -0
  39. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/firecrawl/no_bot_domains.txt +0 -0
  40. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/instagram.py +0 -0
  41. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/mvault.py +0 -0
  42. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/perma_cc.py +0 -0
  43. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/telegram.py +0 -0
  44. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/tiktok.py +0 -0
  45. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/x.py +0 -0
  46. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/youtube.py +0 -0
  47. {scrapemm-0.6.2 → scrapemm-0.6.3}/scrapemm/integrations/ytdlp.py +0 -0
  48. {scrapemm-0.6.2 → scrapemm-0.6.3}/setup.cfg +0 -0
  49. {scrapemm-0.6.2 → scrapemm-0.6.3}/testing/test_retrieval.py +0 -0
  50. {scrapemm-0.6.2 → scrapemm-0.6.3}/testing/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.6.2
3
+ Version: 0.6.3
4
4
  Summary: Flexible multimodal scraper for social media and the open web.
5
5
  Author-email: Mark Rothermel <mark.rothermel@tu-darmstadt.de>
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrapeMM"
3
- version = "0.6.2"
3
+ version = "0.6.3"
4
4
  description = "Flexible multimodal scraper for social media and the open web."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -22,4 +22,5 @@ playwright
22
22
  playwright-stealth
23
23
  beautifulsoup4
24
24
  pip-system-certs # For better SSL certificate handling
25
- backports.zstd # For better content parsing with Firecrawl
25
+ backports.zstd # For better content parsing with Firecrawl
26
+ prompt_toolkit # For handling user terminal input
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapeMM
3
- Version: 0.6.2
3
+ Version: 0.6.3
4
4
  Summary: Flexible multimodal scraper for social media and the open web.
5
5
  Author-email: Mark Rothermel <mark.rothermel@tu-darmstadt.de>
6
6
  License-Expression: Apache-2.0
@@ -26,23 +26,21 @@ class Decodo:
26
26
  DECODO_API_URL = "https://scraper-api.decodo.com/v2/scrape"
27
27
 
28
28
  def __init__(self):
29
- self.username = None
30
- self.password = None
29
+ self.basic_auth_token = None
31
30
  self.n_scrapes = 0
32
31
 
33
- def _load_credentials(self):
32
+ def _load_token(self):
34
33
  """Loads Decodo credentials from the secrets manager."""
35
- self.username = get_secret("decodo_username")
36
- self.password = get_secret("decodo_password")
34
+ self.basic_auth_token = get_secret("decodo_token")
37
35
 
38
- if self.username and self.password:
39
- logger.info("✅ Decodo credentials loaded successfully.")
36
+ if self.basic_auth_token:
37
+ logger.info("✅ Decodo token set.")
40
38
  else:
41
- logger.warning("⚠️ Decodo credentials not found. Please configure them in secrets.")
39
+ logger.warning("⚠️ Decodo auth token not found. Please configure it in secrets.")
42
40
 
43
- def _has_credentials(self) -> bool:
41
+ def _has_token(self) -> bool:
44
42
  """Checks if Decodo credentials are available."""
45
- return bool(self.username and self.password)
43
+ return bool(self.basic_auth_token)
46
44
 
47
45
  async def scrape(
48
46
  self, url: str,
@@ -66,10 +64,10 @@ class Decodo:
66
64
  Returns:
67
65
  MultimodalSequence containing the scraped content, or None if scraping failed
68
66
  """
69
- if not self._has_credentials():
70
- self._load_credentials()
67
+ if not self._has_token():
68
+ self._load_token()
71
69
 
72
- if not self._has_credentials():
70
+ if not self._has_token():
73
71
  logger.warning("Cannot scrape with Decodo: credentials not configured.")
74
72
  return None
75
73
 
@@ -124,6 +122,7 @@ class Decodo:
124
122
  """
125
123
  headers = {
126
124
  'Content-Type': 'application/json',
125
+ 'Authorization': f'Basic {self.basic_auth_token}',
127
126
  }
128
127
 
129
128
  # Build request payload
@@ -141,9 +140,6 @@ class Decodo:
141
140
  if use_premium_proxy:
142
141
  payload["proxy_pool"] = "premium"
143
142
 
144
- # Create basic auth
145
- auth = aiohttp.BasicAuth(self.username, self.password)
146
-
147
143
  # Retry loop with exponential backoff
148
144
  for attempt in range(max_retries + 1):
149
145
  try:
@@ -151,12 +147,11 @@ class Decodo:
151
147
  self.DECODO_API_URL,
152
148
  json=payload,
153
149
  headers=headers,
154
- auth=auth,
155
150
  timeout=aiohttp.ClientTimeout(total=timeout)
156
151
  ) as response:
157
152
  # Validate response health
158
153
  if response.status != 200:
159
- logger.debug("Communication with Decodo API failed.")
154
+ logger.debug(f"Communication with Decodo API failed. Status code: {response.status}")
160
155
 
161
156
  if response.status == 429: # Rate limit
162
157
  if attempt >= max_retries:
@@ -41,6 +41,8 @@ BEST_METHODS = {
41
41
  "perma.cc": ["integrations"],
42
42
  "mvau.lt": ["integrations"],
43
43
  "archive.org": ["integrations"],
44
+ "verafiles.org": ["decodo", "firecrawl"],
45
+ "youturn.in": ["decodo"],
44
46
  # Archive.today:
45
47
  "archive.today": ["integrations", "decodo"],
46
48
  "archive.is": ["integrations", "decodo"],
@@ -185,7 +187,7 @@ async def _retrieve_single(
185
187
  session=session, format=format, actions=actions),
186
188
  "decodo": lambda: decodo.scrape(url, remove_urls, session,
187
189
  format=format,
188
- timeout=10 if prioritize == "speed" else 30,
190
+ timeout=15 if prioritize == "speed" else 30,
189
191
  max_retries=1 if prioritize == "speed" else 5),
190
192
  }
191
193
 
@@ -227,7 +229,7 @@ async def _retrieve_single(
227
229
 
228
230
  except Exception as e:
229
231
  logger.warning(f"Error while retrieving with method '{method_name}': {e}")
230
- errors[method_name] = e
232
+ errors[method_name] = f"{type(e).__name__}: {e}"
231
233
  result = None
232
234
 
233
235
  if result is not None:
@@ -9,8 +9,8 @@ from cryptography.hazmat.backends import default_backend
9
9
  from cryptography.hazmat.primitives import hashes
10
10
  from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
11
11
 
12
- from scrapemm.common import get_config_var, update_config, CONFIG_PATH, CONFIG_DIR
13
- from scrapemm.util import get_multiline_user_input
12
+ from scrapemm.common import get_config_var, update_config, CONFIG_DIR
13
+ from scrapemm.util import get_user_input
14
14
 
15
15
  logger = logging.getLogger("scrapeMM")
16
16
 
@@ -23,8 +23,7 @@ SECRETS = {
23
23
  "bluesky_password": "Bluesky password",
24
24
  "tiktok_client_key": "TikTok client key",
25
25
  "tiktok_client_secret": "TikTok client secret",
26
- "decodo_username": "Decodo Web Scraping API username",
27
- "decodo_password": "Decodo Web Scraping API password",
26
+ "decodo_token": "Decodo Web Scraping API basic authentication token",
28
27
  "youtube_cookie": "YouTube cookie string",
29
28
  "facebook_cookie": "Facebook cookie string",
30
29
  }
@@ -144,9 +143,9 @@ def override_secret(key_name: str):
144
143
  when nothing entered."""
145
144
  description = SECRETS[key_name]
146
145
  if key_name in ["youtube_cookie", "facebook_cookie"]:
147
- user_input = get_multiline_user_input(f"Please enter the {description}. Hit Ctrl-D or Ctrl-Z to save it.")
146
+ user_input = get_user_input(f"Please enter the {description} (Alt+Enter to submit):", multiline=True)
148
147
  else:
149
- user_input = getpass(f"Please enter the {description} (leave empty to skip): ", stream=sys.stdout)
148
+ user_input = get_user_input(f"Please enter the {description} (leave empty to skip):")
150
149
  if user_input:
151
150
  set_secret(key_name, user_input)
152
151
 
@@ -171,8 +170,7 @@ def configure_secrets(all_keys: bool = False):
171
170
 
172
171
  update_config(api_keys_configured=True)
173
172
 
174
- logger.info("API keys configured successfully! If you want to change them, go to "
175
- f"{CONFIG_PATH.as_posix()} and set 'api_keys_configured' to 'false' or "
173
+ logger.info("API keys configured successfully! If you want to change them, "
176
174
  f"run scrapemm.secrets.configure_secrets().")
177
175
 
178
176
 
@@ -38,7 +38,7 @@ def get_domain(url: str, keep_subdomain: bool = False) -> Optional[str]:
38
38
  async def run_with_semaphore(tasks: Iterable[Awaitable],
39
39
  limit: int,
40
40
  show_progress: bool = True,
41
- progress_description: str | None = None) -> list:
41
+ progress_description: str | None = None) -> tuple:
42
42
  """
43
43
  Runs asynchronous tasks with a concurrency limit.
44
44
 
@@ -76,10 +76,32 @@ def read_urls_from_file(file_path):
76
76
  return f.read().splitlines()
77
77
 
78
78
 
79
- def get_multiline_user_input(prompt: str) -> str:
80
- print(prompt)
81
- lines = sys.stdin.readlines()
82
- return "".join(lines)
79
+ def get_user_input(prompt: str, multiline: bool = False) -> str:
80
+ """Prompts the user for input.
81
+ - Single-line: uses standard input(); Enter submits, empty input skips.
82
+ - Multiline: uses prompt_toolkit; Enter adds newline, Alt+Enter submits.
83
+ """
84
+ if multiline:
85
+ from prompt_toolkit import prompt as pt_prompt
86
+ from prompt_toolkit.key_binding import KeyBindings
87
+
88
+ print(prompt)
89
+
90
+ bindings = KeyBindings()
91
+
92
+ @bindings.add('escape', 'enter')
93
+ def _submit(event):
94
+ event.current_buffer.validate_and_handle()
95
+
96
+ result = pt_prompt(
97
+ ">>> ",
98
+ multiline=True,
99
+ key_bindings=bindings,
100
+ prompt_continuation="... ",
101
+ )
102
+ return result
103
+ else:
104
+ return input(f"{prompt} ")
83
105
 
84
106
 
85
107
  MAX_MEDIA_PER_PAGE = 32
@@ -127,7 +149,7 @@ async def resolve_media_hyperlinks(
127
149
 
128
150
  # Try to download media for each URL
129
151
  tasks = [download_medium(u, session=session, headers={"Referer": url}) for u in hrefs_urls.values()]
130
- media: list[Item | None] = await run_with_semaphore(tasks, limit=100, show_progress=False)
152
+ media: tuple[Item | None] = await run_with_semaphore(tasks, limit=100, show_progress=False)
131
153
 
132
154
  href_media = dict(zip(hrefs_urls.keys(), media))
133
155
 
@@ -0,0 +1,11 @@
1
+ from scrapemm import retrieve
2
+ import asyncio
3
+
4
+ if __name__ == "__main__":
5
+ url = "https://youturn.in/factcheck/tamilnadu-india-alliance-final-list.html"
6
+ result = asyncio.run(retrieve(url, methods=["decodo", "firecrawl"]))
7
+ print(f"Retrieval time: {result.retrieval_time:.2f} seconds")
8
+ if result.errors:
9
+ print(result.errors)
10
+ else:
11
+ print(result.content)
@@ -1,10 +0,0 @@
1
- from scrapemm import retrieve
2
- import asyncio
3
-
4
- if __name__ == "__main__":
5
- url = "https://www.snopes.com/fact-check/biden-bounty-maduro/"
6
- result = asyncio.run(retrieve(url, methods=["firecrawl"]))
7
- if result.errors:
8
- print(result.errors)
9
- else:
10
- print(result.content)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes