cobweb-launcher 1.2.13__py3-none-any.whl → 1.2.15__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.

Potentially problematic release.


This version of cobweb-launcher might be problematic. Click here for more details.

@@ -23,28 +23,34 @@ class Crawler(threading.Thread):
23
23
  self,
24
24
  stop: threading.Event,
25
25
  pause: threading.Event,
26
- launcher_queue: Union[Mapping[str, Queue]],
26
+ # launcher_queue: Union[Mapping[str, Queue]],
27
+ get_seed: Callable,
28
+ set_seed: Callable,
29
+ add_seed: Callable,
30
+ delete_seed: Callable,
31
+ upload_data: Callable,
27
32
  custom_func: Union[Mapping[str, Callable]],
28
33
  thread_num: int,
29
34
  max_retries: int,
30
- time_sleep: int
35
+ time_sleep: int,
31
36
  ):
32
37
  super().__init__()
33
38
 
34
39
  self._stop = stop
35
40
  self._pause = pause
36
- self._new = launcher_queue["new"]
37
- self._todo = launcher_queue["todo"]
38
- self._done = launcher_queue["done"]
39
- self._upload = launcher_queue["upload"]
41
+ self._get_seed = get_seed
42
+ self._set_seed = set_seed
43
+ self._add_seed = add_seed
44
+ self._delete_seed = delete_seed
45
+ self._upload_data = upload_data
40
46
 
41
47
  for func_name, _callable in custom_func.items():
42
48
  if isinstance(_callable, Callable):
43
49
  self.__setattr__(func_name, _callable)
44
50
 
45
51
  self.thread_num = thread_num
46
- self.max_retries = max_retries
47
52
  self.time_sleep = time_sleep
53
+ self.max_retries = max_retries
48
54
 
49
55
  @staticmethod
50
56
  def request(seed: Seed) -> Union[Request, BaseItem]:
@@ -66,23 +72,23 @@ class Crawler(threading.Thread):
66
72
 
67
73
  def distribute(self, item, seed):
68
74
  if isinstance(item, BaseItem):
69
- self._upload.push(item)
75
+ self._upload_data(item)
70
76
  elif isinstance(item, Seed):
71
- self._new.push(item)
77
+ self._add_seed(item)
72
78
  elif isinstance(item, str) and item == DealModel.poll:
73
- self._todo.push(seed)
79
+ self._set_seed(seed)
74
80
  elif isinstance(item, str) and item == DealModel.done:
75
- self._done.push(seed)
81
+ self._delete_seed(seed)
76
82
  elif isinstance(item, str) and item == DealModel.fail:
77
83
  seed.params.seed_status = DealModel.fail
78
- self._done.push(seed)
84
+ self._delete_seed(seed)
79
85
  else:
80
86
  raise TypeError("yield value type error!")
81
87
 
82
88
  def spider(self):
83
89
  while not self._stop.is_set():
84
90
 
85
- seed = self._todo.pop()
91
+ seed = self._get_seed()
86
92
 
87
93
  if not seed:
88
94
  time.sleep(1)
@@ -90,7 +96,7 @@ class Crawler(threading.Thread):
90
96
 
91
97
  elif seed.params.retry > self.max_retries:
92
98
  seed.params.seed_status = DealModel.fail
93
- self._done.push(seed)
99
+ self._delete_seed(seed)
94
100
  continue
95
101
 
96
102
  seed_detail_log_info = LogTemplate.log_info(seed.to_dict)
@@ -151,7 +157,8 @@ class Crawler(threading.Thread):
151
157
  exception=''.join(traceback.format_exception(type(e), e, e.__traceback__))
152
158
  ))
153
159
  seed.params.retry += 1
154
- self._todo.push(seed)
160
+ # self._todo.push(seed)
161
+ self._set_seed(seed)
155
162
  time.sleep(self.time_sleep * seed.params.retry)
156
163
  finally:
157
164
  time.sleep(0.1)
cobweb/db/redis_db.py CHANGED
@@ -63,7 +63,7 @@ class RedisDB:
63
63
 
64
64
  def lock(self, key, t=15) -> bool:
65
65
  lua_script = """
66
- local status = redis.call('setnx', KEYS[1], ARGV[1])
66
+ local status = redis.call('setnx', KEYS[1], 1)
67
67
  if ( status == 1 ) then
68
68
  redis.call('expire', KEYS[1], ARGV[1])
69
69
  end
@@ -17,6 +17,8 @@ def check_pause(func):
17
17
  func(self, *args, **kwargs)
18
18
  except Exception as e:
19
19
  logger.info(f"{func.__name__}: " + str(e))
20
+ finally:
21
+ time.sleep(0.1)
20
22
 
21
23
  return wrapper
22
24
 
@@ -95,6 +97,7 @@ class Launcher(threading.Thread):
95
97
  self._spider_max_retries = setting.SPIDER_MAX_RETRIES
96
98
  self._spider_thread_num = setting.SPIDER_THREAD_NUM
97
99
  self._spider_time_sleep = setting.SPIDER_TIME_SLEEP
100
+ self._spider_max_speed = setting.SPIDER_MAX_SPEED
98
101
 
99
102
  self._done_model = setting.DONE_MODEL
100
103
  self._task_model = setting.TASK_MODEL
@@ -156,6 +159,21 @@ class Launcher(threading.Thread):
156
159
  self.__DOING__.pop(seed, None)
157
160
  # logger.info("remove %s seeds from __DOING__" % len(seeds))
158
161
 
162
+ def _get_seed(self) -> Seed:
163
+ return self.__LAUNCHER_QUEUE__["todo"].pop()
164
+
165
+ def _set_seed(self, seed, **kwargs):
166
+ self.__LAUNCHER_QUEUE__["todo"].push(seed, **kwargs)
167
+
168
+ def _upload_data(self, data, **kwargs):
169
+ self.__LAUNCHER_QUEUE__["upload"].push(data, **kwargs)
170
+
171
+ def _add_seed(self, seed, **kwargs):
172
+ self.__LAUNCHER_QUEUE__["new"].push(seed, **kwargs)
173
+
174
+ def _delete_seed(self, seed, **kwargs):
175
+ self.__LAUNCHER_QUEUE__["done"].push(seed, **kwargs)
176
+
159
177
  def _execute(self):
160
178
  for func_name in self.__LAUNCHER_FUNC__:
161
179
  threading.Thread(name=func_name, target=getattr(self, func_name)).start()
@@ -168,7 +186,12 @@ class Launcher(threading.Thread):
168
186
 
169
187
  self._Crawler(
170
188
  stop=self._stop, pause=self._pause,
171
- launcher_queue=self.__LAUNCHER_QUEUE__,
189
+ # launcher_queue=self.__LAUNCHER_QUEUE__,
190
+ get_seed=self._get_seed,
191
+ set_seed=self._set_seed,
192
+ add_seed=self._add_seed,
193
+ delete_seed=self._delete_seed,
194
+ upload_data=self._upload_data,
172
195
  custom_func=self.__CUSTOM_FUNC__,
173
196
  thread_num = self._spider_thread_num,
174
197
  max_retries = self._spider_max_retries,
@@ -16,10 +16,15 @@ class LauncherPro(Launcher):
16
16
  self._done_key = "{%s:%s}:done" % (project, task)
17
17
  self._fail_key = "{%s:%s}:fail" % (project, task)
18
18
  self._heartbeat_key = "heartbeat:%s_%s" % (project, task)
19
- self._reset_lock_key = "lock:reset:%s_%s" % (project, task)
19
+
20
20
  self._statistics_done_key = "statistics:%s:%s:done" % (project, task)
21
21
  self._statistics_fail_key = "statistics:%s:%s:fail" % (project, task)
22
+ self._speed_control_key = "speed_control:%s_%s" % (project, task)
23
+
24
+ self._reset_lock_key = "lock:reset:%s_%s" % (project, task)
25
+
22
26
  self._bf_key = "bloom_%s_%s" % (project, task)
27
+
23
28
  self._db = RedisDB()
24
29
 
25
30
  self._bf = BloomFilter(self._bf_key)
@@ -37,6 +42,21 @@ class LauncherPro(Launcher):
37
42
  else:
38
43
  self._db._client.incrby(key, count)
39
44
 
45
+ def _get_seed(self) -> Seed:
46
+ spider_speed = self._db._client.get(self._speed_control_key)
47
+ if int(spider_speed or 0) > self._spider_max_speed:
48
+ expire_time = self._db.ttl(self._speed_control_key)
49
+ if expire_time == -1:
50
+ self._db.delete(self._speed_control_key)
51
+ else:
52
+ logger.info(f"Too fast! Please wait {expire_time} seconds...")
53
+ time.sleep(expire_time / 2)
54
+ return None
55
+ seed = self.__LAUNCHER_QUEUE__["todo"].pop()
56
+ if seed and not self._db.lock(self._speed_control_key, t=60):
57
+ self._db._client.incrby(self._speed_control_key, 1)
58
+ return seed
59
+
40
60
  @check_pause
41
61
  def _execute_heartbeat(self):
42
62
  if self._heartbeat_start_event.is_set():
@@ -114,7 +134,7 @@ class LauncherPro(Launcher):
114
134
  """
115
135
  删除队列种子,根据状态添加至成功或失败队列,移除doing字典种子索引
116
136
  """
117
- seeds, s_seeds, f_seeds = [], [], []
137
+ seed_info = {"count": 0, "failed": [], "succeed": [], "common": []}
118
138
  status = self.__LAUNCHER_QUEUE__['done'].length < self._done_queue_max_size
119
139
 
120
140
  for _ in range(self._done_queue_max_size):
@@ -122,26 +142,25 @@ class LauncherPro(Launcher):
122
142
  if not seed:
123
143
  break
124
144
  if seed.params.seed_status == DealModel.fail:
125
- f_seeds.append(seed.to_string)
145
+ seed_info["failed"].append(seed.to_string)
126
146
  elif self._done_model == 1:
127
- s_seeds.append(seed.to_string)
147
+ seed_info["succeed"].append(seed.to_string)
128
148
  else:
129
- seeds.append(seed.to_string)
130
- if seeds:
131
- count = self._db.zrem(self._todo_key, *seeds)
132
- if count:
133
- self.statistics(self._statistics_done_key, count)
134
- self._remove_doing_seeds(seeds)
135
- if s_seeds:
136
- count = self._db.done([self._todo_key, self._done_key], *s_seeds)
137
- if count:
138
- self.statistics(self._statistics_done_key, count)
139
- self._remove_doing_seeds(s_seeds)
140
- if f_seeds:
141
- count = self._db.done([self._todo_key, self._fail_key], *f_seeds)
142
- if count:
143
- self.statistics(self._statistics_fail_key, count)
144
- self._remove_doing_seeds(f_seeds)
149
+ seed_info["common"].append(seed.to_string)
150
+ seed_info['count'] += 1
151
+
152
+ if seed_info["count"]:
153
+
154
+ succeed_count = self._db.zrem(self._todo_key, *seed_info["common"])
155
+ succeed_count += self._db.done([self._todo_key, self._done_key], *seed_info["succeed"])
156
+ failed_count = self._db.done([self._todo_key, self._fail_key], *seed_info["failed"])
157
+
158
+ if failed_count:
159
+ self.statistics(self._statistics_fail_key, failed_count)
160
+ if succeed_count:
161
+ self.statistics(self._statistics_done_key, succeed_count)
162
+
163
+ self._remove_doing_seeds(seed_info["common"] + seed_info["succeed"] + seed_info["failed"])
145
164
 
146
165
  if status:
147
166
  time.sleep(self._done_queue_wait_seconds)
cobweb/setting.py CHANGED
@@ -58,6 +58,7 @@ DONE_MODEL = 0 # 0:种子消费成功直接从队列移除,失败则添加
58
58
  SPIDER_THREAD_NUM = 10
59
59
  SPIDER_MAX_RETRIES = 5
60
60
  SPIDER_TIME_SLEEP = 10
61
+ SPIDER_MAX_SPEED = 1000 # 一分钟最大采集数
61
62
 
62
63
  # 任务模式
63
64
  TASK_MODEL = 0 # 0:单次,1:常驻
cobweb/utils/bloom.py CHANGED
@@ -1,56 +1,10 @@
1
+ import math
1
2
  import time
2
3
 
3
-
4
- from redis import Redis
4
+ import mmh3
5
+ import redis
5
6
  from cobweb import setting
6
7
 
7
- # class BloomFilter:
8
- #
9
- # def __init__(self, key, redis_config=None, capacity=None, error_rate=None):
10
- # redis_config = redis_config or setting.REDIS_CONFIG
11
- # capacity = capacity or setting.CAPACITY
12
- # error_rate = error_rate or setting.ERROR_RATE
13
- # redis_config['db'] = 3
14
- #
15
- # self.key = key
16
- #
17
- # pool = redis.ConnectionPool(**redis_config)
18
- # self.bit_size = self.get_bit_size(capacity, error_rate)
19
- # self.hash_count = self.get_hash_count(self.bit_size, capacity)
20
- # self._init_bloom_key()
21
- #
22
- # def add(self, value):
23
- # for seed in range(self.hash_count):
24
- # result = mmh3.hash(value, seed) % self.bit_size
25
- # self._client.setbit(self.key, result, 1)
26
- # return True
27
- #
28
- # def exists(self, value):
29
- # if not self._client.exists(self.key):
30
- # return False
31
- # for seed in range(self.hash_count):
32
- # result = mmh3.hash(value, seed) % self.bit_size
33
- # if not self._client.getbit(self.key, result):
34
- # return False
35
- # return True
36
- #
37
- # def _init_bloom_key(self):
38
- # lua_script = """
39
- # redis.call("SETBIT", KEYS[1], ARGV[1], ARGV[2])
40
- # redis.call("EXPIRE", KEYS[1], 604800)
41
- # """
42
- # if self._client.exists(self.key):
43
- # return True
44
- # execute = self._client.register_script(lua_script)
45
- # execute(keys=[self.key], args=[self.bit_size-1, 1])
46
- #
47
- # @classmethod
48
- # def get_bit_size(cls, n, p):
49
- # return int(-(n * math.log(p)) / (math.log(2) ** 2))
50
- #
51
- # @classmethod
52
- # def get_hash_count(cls, m, n):
53
- # return int((m / n) * math.log(2))
54
8
 
55
9
  class BloomFilter:
56
10
 
@@ -62,37 +16,43 @@ class BloomFilter:
62
16
 
63
17
  self.key = key
64
18
 
65
- self._client = Redis(**redis_config).bf()
66
- self._client.create(key=self.key, capacity=capacity, errorRate=error_rate)
19
+ pool = redis.ConnectionPool(**redis_config)
20
+ self._client = redis.Redis(connection_pool=pool)
21
+ self.bit_size = self.get_bit_size(capacity, error_rate)
22
+ self.hash_count = self.get_hash_count(self.bit_size, capacity)
23
+ self._init_bloom_key()
67
24
 
68
25
  def add(self, value):
69
- return self._client.add(self.key, value)
70
-
71
- def madd(self, items: list):
72
- return self._client.madd(self.key, *items)
26
+ for seed in range(self.hash_count):
27
+ result = mmh3.hash(value, seed) % self.bit_size
28
+ self._client.setbit(self.key, result, 1)
29
+ return True
73
30
 
74
31
  def exists(self, value):
75
- return self._client.exists(self.key, value)
76
-
77
- def mexists(self, items: list):
78
- return self._client.mexists(self.key, *items)
79
-
80
-
81
- if __name__ == '__main__':
82
- testBLF = BloomFilter("test", {
83
- "host": "r-j6c1t3etiefpmz7cwdpd.redis.rds.aliyuncs.com",
84
- "password": "SpiderLinux666",
85
- })
32
+ if not self._client.exists(self.key):
33
+ return False
34
+ for seed in range(self.hash_count):
35
+ result = mmh3.hash(value, seed) % self.bit_size
36
+ if not self._client.getbit(self.key, result):
37
+ return False
38
+ return True
39
+
40
+ def _init_bloom_key(self):
41
+ lua_script = """
42
+ redis.call("SETBIT", KEYS[1], ARGV[1], ARGV[2])
43
+ redis.call("EXPIRE", KEYS[1], 604800)
44
+ """
45
+ if self._client.exists(self.key):
46
+ return True
47
+ execute = self._client.register_script(lua_script)
48
+ execute(keys=[self.key], args=[self.bit_size-1, 1])
49
+
50
+ @classmethod
51
+ def get_bit_size(cls, n, p):
52
+ return int(-(n * math.log(p)) / (math.log(2) ** 2))
53
+
54
+ @classmethod
55
+ def get_hash_count(cls, m, n):
56
+ return int((m / n) * math.log(2))
86
57
 
87
- print("start")
88
- start_time = time.time()
89
- testBLF.add("test")
90
- add_time = time.time()
91
- print("add time::: ")
92
- print(add_time - start_time)
93
- print("get::: ")
94
- print(testBLF.exists("test"))
95
- exist_time = time.time()
96
- print("get time::: ")
97
- print(exist_time - add_time)
98
58
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cobweb-launcher
3
- Version: 1.2.13
3
+ Version: 1.2.15
4
4
  Summary: spider_hole
5
5
  Home-page: https://github.com/Juannie-PP/cobweb
6
6
  Author: Juannie-PP
@@ -1,6 +1,6 @@
1
1
  cobweb/__init__.py,sha256=uMHyf4Fekbyw2xBCbkA8R0LwCpBJf5p_7pWbh60ZWYk,83
2
2
  cobweb/constant.py,sha256=zy3XYsc1qp2B76_Fn_hVQ8eGHlPBd3OFlZK2cryE6FY,2839
3
- cobweb/setting.py,sha256=S33EYK5hrrXn8GFkOmwt9Qn7OdNv8E4h8a0ZyeNydCM,2092
3
+ cobweb/setting.py,sha256=MGe4QGnE5XOTh9Z7NhakaTFK7f-lZtzlA9PFcuc1qoY,2145
4
4
  cobweb/base/__init__.py,sha256=4gwWWQ0Q8cYG9cD7Lwf4XMqRGc5M_mapS3IczR6zeCE,222
5
5
  cobweb/base/common_queue.py,sha256=W7PPZZFl52j3Mc916T0imHj7oAUelA6aKJwW-FecDPE,872
6
6
  cobweb/base/decorators.py,sha256=wDCaQ94aAZGxks9Ljc0aXq6omDXT1_yzFy83ZW6VbVI,930
@@ -11,16 +11,16 @@ cobweb/base/response.py,sha256=eB1DWMXFCpn3cJ3yzgCRU1WeZAdayGDohRgdjdMUFN4,406
11
11
  cobweb/base/seed.py,sha256=Uz_VBRlAxNYQcFHk3tsZFMlU96yPOedHaWGTvk-zKd8,2908
12
12
  cobweb/crawlers/__init__.py,sha256=msvkB9mTpsgyj8JfNMsmwAcpy5kWk_2NrO1Adw2Hkw0,29
13
13
  cobweb/crawlers/base_crawler.py,sha256=ee_WSDnPQpPTk6wlFuY2UEx5L3hcsAZFcr6i3GLSry8,5751
14
- cobweb/crawlers/crawler.py,sha256=5mex-ENuzZSME0EIdwS9fnWkAX6LQuEyoDNcFm0emqs,6132
14
+ cobweb/crawlers/crawler.py,sha256=xiFNM0t69f5xlm59hPbO2MpqtdirVAUhD84-CLpyHPM,6349
15
15
  cobweb/crawlers/file_crawler.py,sha256=2Sjbdgxzqd41WykKUQE3QQlGai3T8k-pmHNmPlTchjQ,4454
16
16
  cobweb/db/__init__.py,sha256=ut0iEyBLjcJL06WNG_5_d4hO5PJWvDrKWMkDOdmgh2M,30
17
- cobweb/db/redis_db.py,sha256=NNI2QkRV1hEZI-z-COEncXt88z3pZN6wusKlcQzc8V4,4304
17
+ cobweb/db/redis_db.py,sha256=fumNZJiio-uQqRcSrymx8eJ1PqsdOwITe_Y-9JOXxrQ,4298
18
18
  cobweb/exceptions/__init__.py,sha256=E9SHnJBbhD7fOgPFMswqyOf8SKRDrI_i25L0bSpohvk,32
19
19
  cobweb/exceptions/oss_db_exception.py,sha256=iP_AImjNHT3-Iv49zCFQ3rdLnlvuHa3h2BXApgrOYpA,636
20
20
  cobweb/launchers/__init__.py,sha256=af0Y6wrGX8SQZ7w7XL2sOtREjCT3dwad-uCc3nIontY,76
21
- cobweb/launchers/launcher.py,sha256=sH9bj5TIPX5RjpNFvHmqTtlhyBxQqbTDhuvYSd2LspI,6114
21
+ cobweb/launchers/launcher.py,sha256=AbkrytfJEyj8FhTbLgjmOOIuvOYV3cpVknE9yt31WbM,6930
22
22
  cobweb/launchers/launcher_air.py,sha256=KAk_M8F3029cXYe7m4nn3Nzyi89lbxJ2cqZjqW8iZ0E,2832
23
- cobweb/launchers/launcher_pro.py,sha256=4hi8xviIJr8HqTERShfdZjp767KXBeIyqyHaW9rYOhE,7815
23
+ cobweb/launchers/launcher_pro.py,sha256=8QKhToKoD2WonIaqRQAhUWRhbNOIgYXzGFRK1id_3yM,8638
24
24
  cobweb/pipelines/__init__.py,sha256=zSUsGtx6smbs2iXBXvYynReKSgky-3gjqaAtKVnA_OU,105
25
25
  cobweb/pipelines/base_pipeline.py,sha256=fYnWf79GmhufXpcnMa3te18SbmnVeYLwxfyo-zLd9CY,1577
26
26
  cobweb/pipelines/loghub_pipeline.py,sha256=cjPO6w6UJ0jNw2fVvdX0BCdlm58T7dmYXlxzXOBpvfY,1027
@@ -28,11 +28,11 @@ cobweb/pipelines/pipeline.py,sha256=4TJLX0sUHRxYndF5A4Vs5btUGI-wigkOcFvhTW1hLXI,
28
28
  cobweb/pipelines/pipeline_console.py,sha256=NEh-4zhuVAQOqwXLsqeb-rcNZ9_KXFUpL3otUTL5qBs,754
29
29
  cobweb/pipelines/pipeline_loghub.py,sha256=xZ6D55BGdiM71WUv83jyLGbEyUwhBHLJRZoXthBxxTs,1019
30
30
  cobweb/utils/__init__.py,sha256=vBtZTy3EfRE0MmH43URhmr7nw6_oOWTEbGOM9xR_9o8,78
31
- cobweb/utils/bloom.py,sha256=G0PlaMVTz6KCmhcNToi28bAHj1YJjDVqwJAQ_DUBWGk,3030
31
+ cobweb/utils/bloom.py,sha256=vng-YbKgh9HbtpAWYf_nkUSbfVTOj40aqUUejRYlsCU,1752
32
32
  cobweb/utils/oss.py,sha256=gyt8-UB07tVphZLQXMOf-JTJwU-mWq8KZkOXKkAf3uk,3513
33
33
  cobweb/utils/tools.py,sha256=5JEaaAwYoV9Sdla2UBIJn6faUBuXmxUMagm9ck6FVqs,1253
34
- cobweb_launcher-1.2.13.dist-info/LICENSE,sha256=z1rxSIGOyzcSb3orZxFPxzx-0C1vTocmswqBNxpKfEk,1063
35
- cobweb_launcher-1.2.13.dist-info/METADATA,sha256=-HRZ-uipPBajhDyuDF2TTtQQAectd1gsjk3KwvWtHpo,6510
36
- cobweb_launcher-1.2.13.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
37
- cobweb_launcher-1.2.13.dist-info/top_level.txt,sha256=4GETBGNsKqiCUezmT-mJn7tjhcDlu7nLIV5gGgHBW4I,7
38
- cobweb_launcher-1.2.13.dist-info/RECORD,,
34
+ cobweb_launcher-1.2.15.dist-info/LICENSE,sha256=z1rxSIGOyzcSb3orZxFPxzx-0C1vTocmswqBNxpKfEk,1063
35
+ cobweb_launcher-1.2.15.dist-info/METADATA,sha256=rP73tkfQPQB8MCEYg31ZOdEbBYUUoyXzAcUevfnwqec,6510
36
+ cobweb_launcher-1.2.15.dist-info/WHEEL,sha256=ewwEueio1C2XeHTvT17n8dZUJgOvyCWCt0WVNLClP9o,92
37
+ cobweb_launcher-1.2.15.dist-info/top_level.txt,sha256=4GETBGNsKqiCUezmT-mJn7tjhcDlu7nLIV5gGgHBW4I,7
38
+ cobweb_launcher-1.2.15.dist-info/RECORD,,