spiderforce4ai 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
@@ -57,22 +57,27 @@ class CrawlConfig:
57
57
  output_dir: Path = Path("spiderforce_reports") # Default to spiderforce_reports in current directory
58
58
  webhook_url: Optional[str] = None # Optional webhook endpoint
59
59
  webhook_timeout: int = 10 # Webhook timeout
60
- report_file: Optional[Path] = None # Optional report file location
60
+ webhook_headers: Optional[Dict[str, str]] = None # Optional webhook headers
61
+ webhook_payload_template: Optional[str] = None # Optional custom webhook payload template
62
+ save_reports: bool = False # Whether to save crawl reports
63
+ report_file: Optional[Path] = None # Optional report file location (used only if save_reports is True)
61
64
 
62
65
  def __post_init__(self):
63
- # Initialize empty lists for selectors if None
66
+ # Initialize empty lists/dicts for None values
64
67
  self.remove_selectors = self.remove_selectors or []
65
68
  self.remove_selectors_regex = self.remove_selectors_regex or []
69
+ self.webhook_headers = self.webhook_headers or {}
66
70
 
67
71
  # Ensure output_dir is a Path and exists
68
72
  self.output_dir = Path(self.output_dir)
69
73
  self.output_dir.mkdir(parents=True, exist_ok=True)
70
74
 
71
- # If report_file is not specified, create it in output_dir
72
- if self.report_file is None:
73
- self.report_file = self.output_dir / "crawl_report.json"
74
- else:
75
- self.report_file = Path(self.report_file)
75
+ # Only setup report file if save_reports is True
76
+ if self.save_reports:
77
+ if self.report_file is None:
78
+ self.report_file = self.output_dir / "crawl_report.json"
79
+ else:
80
+ self.report_file = Path(self.report_file)
76
81
 
77
82
  def to_dict(self) -> Dict:
78
83
  """Convert config to dictionary for API requests."""
@@ -92,19 +97,34 @@ def _send_webhook_sync(result: CrawlResult, config: CrawlConfig) -> None:
92
97
  if not config.webhook_url:
93
98
  return
94
99
 
95
- payload = {
96
- "url": result.url,
97
- "status": result.status,
98
- "markdown": result.markdown if result.status == "success" else None,
99
- "error": result.error if result.status == "failed" else None,
100
- "timestamp": result.timestamp,
101
- "config": config.to_dict()
102
- }
100
+ # Use custom payload template if provided, otherwise use default
101
+ if config.webhook_payload_template:
102
+ # Replace variables in the template
103
+ payload_str = config.webhook_payload_template.format(
104
+ url=result.url,
105
+ status=result.status,
106
+ markdown=result.markdown if result.status == "success" else None,
107
+ error=result.error if result.status == "failed" else None,
108
+ timestamp=result.timestamp,
109
+ config=config.to_dict()
110
+ )
111
+ payload = json.loads(payload_str) # Parse the formatted JSON string
112
+ else:
113
+ # Use default payload format
114
+ payload = {
115
+ "url": result.url,
116
+ "status": result.status,
117
+ "markdown": result.markdown if result.status == "success" else None,
118
+ "error": result.error if result.status == "failed" else None,
119
+ "timestamp": result.timestamp,
120
+ "config": config.to_dict()
121
+ }
103
122
 
104
123
  try:
105
124
  response = requests.post(
106
125
  config.webhook_url,
107
126
  json=payload,
127
+ headers=config.webhook_headers,
108
128
  timeout=config.webhook_timeout
109
129
  )
110
130
  response.raise_for_status()
@@ -196,6 +216,113 @@ class SpiderForce4AI:
196
216
  await f.write(markdown)
197
217
  return filepath
198
218
 
219
+
220
+
221
+ def crawl_sitemap_server_parallel(self, sitemap_url: str, config: CrawlConfig) -> List[CrawlResult]:
222
+ """
223
+ Crawl sitemap URLs using server-side parallel processing.
224
+ """
225
+ print(f"Fetching sitemap from {sitemap_url}...")
226
+
227
+ # Fetch sitemap
228
+ try:
229
+ response = requests.get(sitemap_url, timeout=config.timeout)
230
+ response.raise_for_status()
231
+ sitemap_text = response.text
232
+ except Exception as e:
233
+ print(f"Error fetching sitemap: {str(e)}")
234
+ raise
235
+
236
+ # Parse sitemap
237
+ try:
238
+ root = ET.fromstring(sitemap_text)
239
+ namespace = {'ns': root.tag.split('}')[0].strip('{')}
240
+ urls = [loc.text for loc in root.findall('.//ns:loc', namespace)]
241
+ print(f"Found {len(urls)} URLs in sitemap")
242
+ except Exception as e:
243
+ print(f"Error parsing sitemap: {str(e)}")
244
+ raise
245
+
246
+ # Process URLs using server-side parallel endpoint
247
+ return self.crawl_urls_server_parallel(urls, config)
248
+
249
+
250
+ def crawl_urls_server_parallel(self, urls: List[str], config: CrawlConfig) -> List[CrawlResult]:
251
+ """
252
+ Crawl multiple URLs using server-side parallel processing.
253
+ This uses the /convert_parallel endpoint which handles parallelization on the server.
254
+ """
255
+ print(f"Sending {len(urls)} URLs for parallel processing...")
256
+
257
+ try:
258
+ endpoint = f"{self.base_url}/convert_parallel"
259
+
260
+ # Prepare payload
261
+ payload = {
262
+ "urls": urls,
263
+ **config.to_dict()
264
+ }
265
+
266
+ # Send request
267
+ response = requests.post(
268
+ endpoint,
269
+ json=payload,
270
+ timeout=config.timeout
271
+ )
272
+ response.raise_for_status()
273
+
274
+ # Process results
275
+ results = []
276
+ server_results = response.json() # Assuming server returns JSON array of results
277
+
278
+ for url_result in server_results:
279
+ result = CrawlResult(
280
+ url=url_result["url"],
281
+ status=url_result.get("status", "failed"),
282
+ markdown=url_result.get("markdown"),
283
+ error=url_result.get("error"),
284
+ config=config.to_dict()
285
+ )
286
+
287
+ # Save markdown if successful and output dir is configured
288
+ if result.status == "success" and config.output_dir and result.markdown:
289
+ filepath = config.output_dir / f"{slugify(result.url)}.md"
290
+ with open(filepath, 'w', encoding='utf-8') as f:
291
+ f.write(result.markdown)
292
+
293
+ # Send webhook if configured
294
+ if config.webhook_url:
295
+ _send_webhook_sync(result, config)
296
+
297
+ results.append(result)
298
+
299
+ # Save report if enabled
300
+ if config.save_reports:
301
+ self._save_report_sync(results, config)
302
+ print(f"\nReport saved to: {config.report_file}")
303
+
304
+ # Print summary
305
+ successful = len([r for r in results if r.status == "success"])
306
+ failed = len([r for r in results if r.status == "failed"])
307
+ print(f"\nParallel processing completed:")
308
+ print(f"✓ Successful: {successful}")
309
+ print(f"✗ Failed: {failed}")
310
+
311
+ return results
312
+
313
+ except Exception as e:
314
+ print(f"Error during parallel processing: {str(e)}")
315
+ # Create failed results for all URLs
316
+ return [
317
+ CrawlResult(
318
+ url=url,
319
+ status="failed",
320
+ error=str(e),
321
+ config=config.to_dict()
322
+ ) for url in urls
323
+ ]
324
+
325
+
199
326
  async def _send_webhook(self, result: CrawlResult, config: CrawlConfig):
200
327
  """Send webhook with crawl results."""
201
328
  if not config.webhook_url:
@@ -313,6 +440,55 @@ class SpiderForce4AI:
313
440
  """Synchronous version of crawl_url_async."""
314
441
  return asyncio.run(self.crawl_url_async(url, config))
315
442
 
443
+ async def _retry_failed_urls(self, failed_results: List[CrawlResult], config: CrawlConfig, progress=None) -> List[CrawlResult]:
444
+ """Retry failed URLs once."""
445
+ if not failed_results:
446
+ return []
447
+
448
+ console.print("\n[yellow]Retrying failed URLs...[/yellow]")
449
+ retry_results = []
450
+
451
+ # Create a new progress bar if one wasn't provided
452
+ should_close_progress = progress is None
453
+ if progress is None:
454
+ progress = Progress(
455
+ SpinnerColumn(),
456
+ TextColumn("[progress.description]{task.description}"),
457
+ BarColumn(),
458
+ TaskProgressColumn(),
459
+ console=console
460
+ )
461
+ progress.start()
462
+
463
+ retry_task = progress.add_task("[yellow]Retrying failed URLs...", total=len(failed_results))
464
+
465
+ for result in failed_results:
466
+ progress.update(retry_task, description=f"[yellow]Retrying: {result.url}")
467
+
468
+ try:
469
+ new_result = await self.crawl_url_async(result.url, config)
470
+ if new_result.status == "success":
471
+ console.print(f"[green]✓ Retry successful: {result.url}[/green]")
472
+ else:
473
+ console.print(f"[red]✗ Retry failed: {result.url} - {new_result.error}[/red]")
474
+ retry_results.append(new_result)
475
+ except Exception as e:
476
+ console.print(f"[red]✗ Retry error: {result.url} - {str(e)}[/red]")
477
+ retry_results.append(CrawlResult(
478
+ url=result.url,
479
+ status="failed",
480
+ error=f"Retry error: {str(e)}",
481
+ config=config.to_dict()
482
+ ))
483
+
484
+ progress.update(retry_task, advance=1)
485
+ await asyncio.sleep(config.request_delay)
486
+
487
+ if should_close_progress:
488
+ progress.stop()
489
+
490
+ return retry_results
491
+
316
492
  async def crawl_urls_async(self, urls: List[str], config: CrawlConfig) -> List[CrawlResult]:
317
493
  """Crawl multiple URLs asynchronously with progress bar."""
318
494
  await self._ensure_session()
@@ -338,15 +514,27 @@ class SpiderForce4AI:
338
514
  await asyncio.sleep(config.request_delay)
339
515
  return result
340
516
 
341
- results = await asyncio.gather(*[crawl_with_semaphore(url) for url in urls])
517
+ initial_results = await asyncio.gather(*[crawl_with_semaphore(url) for url in urls])
518
+
519
+ # Identify failed URLs
520
+ failed_results = [r for r in initial_results if r.status == "failed"]
521
+
522
+ # Retry failed URLs
523
+ if failed_results:
524
+ retry_results = await self._retry_failed_urls(failed_results, config, progress)
525
+
526
+ # Replace failed results with retry results
527
+ results = [r for r in initial_results if r.status == "success"] + retry_results
528
+ else:
529
+ results = initial_results
342
530
 
343
531
  # Save final report
344
532
  await self._save_report(config)
345
533
 
346
- # Print summary
534
+ # Print final summary
347
535
  successful = len([r for r in results if r.status == "success"])
348
536
  failed = len([r for r in results if r.status == "failed"])
349
- console.print(f"\n[green]Crawling completed:[/green]")
537
+ console.print(f"\n[green]Final crawling results:[/green]")
350
538
  console.print(f"✓ Successful: {successful}")
351
539
  console.print(f"✗ Failed: {failed}")
352
540
 
@@ -436,12 +624,25 @@ class SpiderForce4AI:
436
624
  self._save_report_sync(results, config)
437
625
  print(f"\nReport saved to: {config.report_file}")
438
626
 
439
- # Print summary
627
+ # Identify failed URLs and retry them
628
+ failed_results = [r for r in results if r.status == "failed"]
629
+ if failed_results:
630
+ console.print("\n[yellow]Retrying failed URLs...[/yellow]")
631
+ for result in failed_results:
632
+ new_result = _process_url_parallel((result.url, self.base_url, config))
633
+ if new_result.status == "success":
634
+ console.print(f"[green]✓ Retry successful: {result.url}[/green]")
635
+ # Replace the failed result with the successful retry
636
+ results[results.index(result)] = new_result
637
+ else:
638
+ console.print(f"[red]✗ Retry failed: {result.url} - {new_result.error}[/red]")
639
+
640
+ # Print final summary
440
641
  successful = len([r for r in results if r.status == "success"])
441
642
  failed = len([r for r in results if r.status == "failed"])
442
- print(f"\nCrawling completed:")
443
- print(f"✓ Successful: {successful}")
444
- print(f"✗ Failed: {failed}")
643
+ console.print(f"\n[green]Final crawling results:[/green]")
644
+ console.print(f"✓ Successful: {successful}")
645
+ console.print(f"✗ Failed: {failed}")
445
646
 
446
647
  return results
447
648
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: spiderforce4ai
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: Python wrapper for SpiderForce4AI HTML-to-Markdown conversion service
5
5
  Home-page: https://petertam.pro
6
6
  Author: Piotr Tamulewicz
@@ -24,75 +24,73 @@ Dynamic: requires-python
24
24
 
25
25
  # SpiderForce4AI Python Wrapper
26
26
 
27
- A Python wrapper for SpiderForce4AI - a powerful HTML-to-Markdown conversion service. This package provides an easy-to-use interface for crawling websites and converting their content to clean Markdown format.
28
-
29
- ## Installation
30
-
31
- ```bash
32
- pip install spiderforce4ai
33
- ```
27
+ A Python package for web content crawling and HTML-to-Markdown conversion. Built for seamless integration with SpiderForce4AI service.
34
28
 
35
29
  ## Quick Start (Minimal Setup)
36
30
 
37
31
  ```python
38
32
  from spiderforce4ai import SpiderForce4AI, CrawlConfig
39
33
 
40
- # Initialize with your SpiderForce4AI service URL
34
+ # Initialize with your service URL
41
35
  spider = SpiderForce4AI("http://localhost:3004")
42
36
 
43
- # Use default configuration (will save in ./spiderforce_reports)
37
+ # Create default config
44
38
  config = CrawlConfig()
45
39
 
46
40
  # Crawl a single URL
47
41
  result = spider.crawl_url("https://example.com", config)
48
42
  ```
49
43
 
44
+ ## Installation
45
+
46
+ ```bash
47
+ pip install spiderforce4ai
48
+ ```
49
+
50
50
  ## Crawling Methods
51
51
 
52
- ### 1. Single URL Crawling
52
+ ### 1. Single URL
53
53
 
54
54
  ```python
55
- # Synchronous
55
+ # Basic usage
56
56
  result = spider.crawl_url("https://example.com", config)
57
57
 
58
- # Asynchronous
58
+ # Async version
59
59
  async def crawl():
60
60
  result = await spider.crawl_url_async("https://example.com", config)
61
61
  ```
62
62
 
63
- ### 2. Multiple URLs Crawling
63
+ ### 2. Multiple URLs
64
64
 
65
65
  ```python
66
- # List of URLs
67
66
  urls = [
68
67
  "https://example.com/page1",
69
- "https://example.com/page2",
70
- "https://example.com/page3"
68
+ "https://example.com/page2"
71
69
  ]
72
70
 
73
- # Synchronous
74
- results = spider.crawl_urls(urls, config)
71
+ # Client-side parallel (using multiprocessing)
72
+ results = spider.crawl_urls_parallel(urls, config)
73
+
74
+ # Server-side parallel (single request)
75
+ results = spider.crawl_urls_server_parallel(urls, config)
75
76
 
76
- # Asynchronous
77
+ # Async version
77
78
  async def crawl():
78
79
  results = await spider.crawl_urls_async(urls, config)
79
-
80
- # Parallel (using multiprocessing)
81
- results = spider.crawl_urls_parallel(urls, config)
82
80
  ```
83
81
 
84
82
  ### 3. Sitemap Crawling
85
83
 
86
84
  ```python
87
- # Synchronous
88
- results = spider.crawl_sitemap("https://example.com/sitemap.xml", config)
85
+ # Server-side parallel (recommended)
86
+ results = spider.crawl_sitemap_server_parallel("https://example.com/sitemap.xml", config)
87
+
88
+ # Client-side parallel
89
+ results = spider.crawl_sitemap_parallel("https://example.com/sitemap.xml", config)
89
90
 
90
- # Asynchronous
91
+ # Async version
91
92
  async def crawl():
92
93
  results = await spider.crawl_sitemap_async("https://example.com/sitemap.xml", config)
93
-
94
- # Parallel (using multiprocessing)
95
- results = spider.crawl_sitemap_parallel("https://example.com/sitemap.xml", config)
96
94
  ```
97
95
 
98
96
  ## Configuration Options
@@ -100,9 +98,11 @@ results = spider.crawl_sitemap_parallel("https://example.com/sitemap.xml", confi
100
98
  All configuration options are optional with sensible defaults:
101
99
 
102
100
  ```python
101
+ from pathlib import Path
102
+
103
103
  config = CrawlConfig(
104
104
  # Content Selection (all optional)
105
- target_selector="article", # Specific element to target
105
+ target_selector="article", # Specific element to extract
106
106
  remove_selectors=[ # Elements to remove
107
107
  ".ads",
108
108
  "#popup",
@@ -112,21 +112,34 @@ config = CrawlConfig(
112
112
  remove_selectors_regex=["modal-\\d+"], # Regex patterns for removal
113
113
 
114
114
  # Processing Settings
115
- max_concurrent_requests=1, # Default: 1 (parallel processing)
116
- request_delay=0.5, # Delay between requests in seconds
117
- timeout=30, # Request timeout in seconds
115
+ max_concurrent_requests=1, # For client-side parallel processing
116
+ request_delay=0.5, # Delay between requests (seconds)
117
+ timeout=30, # Request timeout (seconds)
118
118
 
119
119
  # Output Settings
120
- output_dir="custom_output", # Default: "spiderforce_reports"
121
- report_file="custom_report.json", # Default: "crawl_report.json"
122
- webhook_url="https://your-webhook.com", # Optional webhook endpoint
123
- webhook_timeout=10 # Webhook timeout in seconds
120
+ output_dir=Path("spiderforce_reports"), # Default directory for files
121
+ webhook_url="https://your-webhook.com", # Real-time notifications
122
+ webhook_timeout=10, # Webhook timeout
123
+ webhook_headers={ # Optional custom headers for webhook
124
+ "Authorization": "Bearer your-token",
125
+ "X-Custom-Header": "value"
126
+ },
127
+ webhook_payload_template='''{ # Optional custom webhook payload template
128
+ "crawled_url": "{url}",
129
+ "content": "{markdown}",
130
+ "crawl_status": "{status}",
131
+ "crawl_error": "{error}",
132
+ "crawl_time": "{timestamp}",
133
+ "custom_field": "your-value"
134
+ }''',
135
+ save_reports=False, # Whether to save crawl reports (default: False)
136
+ report_file=Path("crawl_report.json") # Report location (used only if save_reports=True)
124
137
  )
125
138
  ```
126
139
 
127
140
  ## Real-World Examples
128
141
 
129
- ### 1. Basic Website Crawling
142
+ ### 1. Basic Blog Crawling
130
143
 
131
144
  ```python
132
145
  from spiderforce4ai import SpiderForce4AI, CrawlConfig
@@ -134,78 +147,77 @@ from pathlib import Path
134
147
 
135
148
  spider = SpiderForce4AI("http://localhost:3004")
136
149
  config = CrawlConfig(
150
+ target_selector="article.post-content",
137
151
  output_dir=Path("blog_content")
138
152
  )
139
153
 
140
- result = spider.crawl_url("https://example.com/blog", config)
141
- print(f"Content saved to: {result.url}.md")
154
+ result = spider.crawl_url("https://example.com/blog-post", config)
142
155
  ```
143
156
 
144
- ### 2. Advanced Parallel Sitemap Crawling
157
+ ### 2. Parallel Website Crawling
145
158
 
146
159
  ```python
147
160
  config = CrawlConfig(
148
- max_concurrent_requests=5,
149
- output_dir=Path("website_content"),
150
161
  remove_selectors=[
151
162
  ".navigation",
152
163
  ".footer",
153
164
  ".ads",
154
165
  "#cookie-notice"
155
166
  ],
167
+ max_concurrent_requests=5,
168
+ output_dir=Path("website_content"),
156
169
  webhook_url="https://your-webhook.com/endpoint"
157
170
  )
158
171
 
159
- results = spider.crawl_sitemap_parallel(
160
- "https://example.com/sitemap.xml",
161
- config
162
- )
172
+ # Using server-side parallel processing
173
+ results = spider.crawl_urls_server_parallel([
174
+ "https://example.com/page1",
175
+ "https://example.com/page2",
176
+ "https://example.com/page3"
177
+ ], config)
163
178
  ```
164
179
 
165
- ### 3. Async Crawling with Progress
180
+ ### 3. Full Sitemap Processing
166
181
 
167
182
  ```python
168
- import asyncio
169
-
170
- async def main():
171
- config = CrawlConfig(
172
- max_concurrent_requests=3,
173
- request_delay=1.0
174
- )
175
-
176
- async with spider:
177
- results = await spider.crawl_urls_async([
178
- "https://example.com/1",
179
- "https://example.com/2",
180
- "https://example.com/3"
181
- ], config)
182
-
183
- return results
183
+ config = CrawlConfig(
184
+ target_selector="main",
185
+ remove_selectors=[".sidebar", ".comments"],
186
+ output_dir=Path("site_content"),
187
+ report_file=Path("crawl_report.json")
188
+ )
184
189
 
185
- results = asyncio.run(main())
190
+ results = spider.crawl_sitemap_server_parallel(
191
+ "https://example.com/sitemap.xml",
192
+ config
193
+ )
186
194
  ```
187
195
 
188
196
  ## Output Structure
189
197
 
190
- ### 1. File Organization
198
+ ### 1. Directory Layout
191
199
  ```
192
- output_dir/
193
- ├── example-com-page1.md
200
+ spiderforce_reports/ # Default output directory
201
+ ├── example-com-page1.md # Converted markdown files
194
202
  ├── example-com-page2.md
195
- └── crawl_report.json
203
+ └── crawl_report.json # Crawl report
196
204
  ```
197
205
 
198
206
  ### 2. Markdown Files
199
- Each markdown file is named using a slugified version of the URL and contains the converted content.
207
+ Each file is named using a slugified version of the URL:
208
+ ```markdown
209
+ # Page Title
210
+
211
+ Content converted to clean markdown...
212
+ ```
200
213
 
201
- ### 3. Report JSON Structure
214
+ ### 3. Crawl Report
202
215
  ```json
203
216
  {
204
217
  "timestamp": "2025-02-15T10:30:00.123456",
205
218
  "config": {
206
219
  "target_selector": "article",
207
- "remove_selectors": [".ads", "#popup"],
208
- "remove_selectors_regex": ["modal-\\d+"]
220
+ "remove_selectors": [".ads", "#popup"]
209
221
  },
210
222
  "results": {
211
223
  "successful": [
@@ -234,7 +246,7 @@ Each markdown file is named using a slugified version of the URL and contains th
234
246
  ```
235
247
 
236
248
  ### 4. Webhook Notifications
237
- If configured, webhooks receive real-time updates in JSON format:
249
+ If configured, real-time updates are sent for each processed URL:
238
250
  ```json
239
251
  {
240
252
  "url": "https://example.com/page1",
@@ -250,7 +262,7 @@ If configured, webhooks receive real-time updates in JSON format:
250
262
 
251
263
  ## Error Handling
252
264
 
253
- The package handles various types of errors:
265
+ The package handles various types of errors gracefully:
254
266
  - Network errors
255
267
  - Timeout errors
256
268
  - Invalid URLs
@@ -269,6 +281,25 @@ All errors are:
269
281
  - Running SpiderForce4AI service
270
282
  - Internet connection
271
283
 
284
+ ## Performance Considerations
285
+
286
+ 1. Server-side Parallel Processing
287
+ - Best for most cases
288
+ - Single HTTP request for multiple URLs
289
+ - Less network overhead
290
+ - Use: `crawl_urls_server_parallel()` or `crawl_sitemap_server_parallel()`
291
+
292
+ 2. Client-side Parallel Processing
293
+ - Good for special cases requiring local control
294
+ - Uses Python multiprocessing
295
+ - More network overhead
296
+ - Use: `crawl_urls_parallel()` or `crawl_sitemap_parallel()`
297
+
298
+ 3. Async Processing
299
+ - Best for integration with async applications
300
+ - Good for real-time processing
301
+ - Use: `crawl_url_async()`, `crawl_urls_async()`, or `crawl_sitemap_async()`
302
+
272
303
  ## License
273
304
 
274
305
  MIT License
@@ -0,0 +1,5 @@
1
+ spiderforce4ai/__init__.py,sha256=oU_UIdzsQxExaVgD7NCaVm4G-9zMtKGnREfY6xL1uFY,26041
2
+ spiderforce4ai-0.1.9.dist-info/METADATA,sha256=poV1i_-H3AgzFhs9juRDJSfaWO0gVePb5JXN7ynL4Y4,7771
3
+ spiderforce4ai-0.1.9.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
4
+ spiderforce4ai-0.1.9.dist-info/top_level.txt,sha256=Kth7A21Js7DCp0j5XBBi-FE45SCLouZkeNZU__Yr9Yk,15
5
+ spiderforce4ai-0.1.9.dist-info/RECORD,,
@@ -1,5 +0,0 @@
1
- spiderforce4ai/__init__.py,sha256=qLYHahjvFutdGmibbVZ7cfTd1mMM1FZNd_7nv-EMPtQ,17649
2
- spiderforce4ai-0.1.7.dist-info/METADATA,sha256=-eWd9exoMxMAYClp6rWHaX_H3md4hBlRq6CHhTJ1ACg,6575
3
- spiderforce4ai-0.1.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
4
- spiderforce4ai-0.1.7.dist-info/top_level.txt,sha256=Kth7A21Js7DCp0j5XBBi-FE45SCLouZkeNZU__Yr9Yk,15
5
- spiderforce4ai-0.1.7.dist-info/RECORD,,