mdbq 4.0.9__tar.gz → 4.0.10__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 (36) hide show
  1. {mdbq-4.0.9 → mdbq-4.0.10}/PKG-INFO +1 -1
  2. mdbq-4.0.10/mdbq/__version__.py +1 -0
  3. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/aggregation/query_data.py +2 -2
  4. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/mysql/s_query.py +183 -144
  5. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq.egg-info/PKG-INFO +1 -1
  6. mdbq-4.0.9/mdbq/__version__.py +0 -1
  7. {mdbq-4.0.9 → mdbq-4.0.10}/README.txt +0 -0
  8. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/__init__.py +0 -0
  9. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/aggregation/__init__.py +0 -0
  10. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/config/__init__.py +0 -0
  11. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/config/config.py +0 -0
  12. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/log/__init__.py +0 -0
  13. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/log/mylogger.py +0 -0
  14. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/log/spider_logging.py +0 -0
  15. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/mysql/__init__.py +0 -0
  16. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/mysql/deduplicator.py +0 -0
  17. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/mysql/mysql.py +0 -0
  18. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/mysql/unique_.py +0 -0
  19. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/mysql/uploader.py +0 -0
  20. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/other/__init__.py +0 -0
  21. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/other/download_sku_picture.py +0 -0
  22. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/other/otk.py +0 -0
  23. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/other/pov_city.py +0 -0
  24. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/other/ua_sj.py +0 -0
  25. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/pbix/__init__.py +0 -0
  26. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/pbix/pbix_refresh.py +0 -0
  27. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/pbix/refresh_all.py +0 -0
  28. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/redis/__init__.py +0 -0
  29. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/redis/getredis.py +0 -0
  30. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/spider/__init__.py +0 -0
  31. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq/spider/aikucun.py +0 -0
  32. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq.egg-info/SOURCES.txt +0 -0
  33. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq.egg-info/dependency_links.txt +0 -0
  34. {mdbq-4.0.9 → mdbq-4.0.10}/mdbq.egg-info/top_level.txt +0 -0
  35. {mdbq-4.0.9 → mdbq-4.0.10}/setup.cfg +0 -0
  36. {mdbq-4.0.9 → mdbq-4.0.10}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mdbq
3
- Version: 4.0.9
3
+ Version: 4.0.10
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -0,0 +1 @@
1
+ VERSION = '4.0.10'
@@ -1557,7 +1557,6 @@ class MysqlDatasQuery:
1557
1557
  'unique_keys': [['日期', '店铺id', '商品id']], # 唯一约束列表
1558
1558
  }
1559
1559
 
1560
-
1561
1560
  @upload_data_decorator()
1562
1561
  def spph(self, db_name='聚合数据', table_name='天猫_商品排行'):
1563
1562
  """ """
@@ -3685,11 +3684,12 @@ def main(months=3):
3685
3684
  password=password,
3686
3685
  host=host,
3687
3686
  port=port,
3688
- maxconnections=30,
3687
+ maxconnections=20,
3689
3688
  )
3690
3689
  query1(download_manager=download_manager, months=months)
3691
3690
  query2(download_manager=download_manager, months=months)
3692
3691
  query3(download_manager=download_manager, months=months)
3692
+ logger.info('数据聚合完成')
3693
3693
 
3694
3694
 
3695
3695
  if __name__ == '__main__':
@@ -47,9 +47,9 @@ class QueryDatas:
47
47
  host: 数据库主机
48
48
  port: 数据库端口
49
49
  charset: 字符集,默认utf8mb4
50
- maxconnections: 最大连接数,默认20
51
- mincached: 最小缓存连接数,默认2
52
- maxcached: 最大缓存连接数,默认5
50
+ maxconnections: 最大活动连接数,默认20
51
+ mincached: 最小缓存连接数,空闲连接数量,默认2
52
+ maxcached: 最大缓存连接数,最大空闲连接数,默认5
53
53
  connect_timeout: 连接超时时间,默认10秒
54
54
  read_timeout: 读取超时时间,默认30秒
55
55
  write_timeout: 写入超时时间,默认30秒
@@ -253,20 +253,8 @@ class QueryDatas:
253
253
 
254
254
  # @_execute_with_retry
255
255
  def _get_connection(self, db_name: Optional[str] = None) -> pymysql.connections.Connection:
256
- """
257
- 从连接池获取数据库连接
258
-
259
- Args:
260
- db_name: 可选的数据库名,如果提供则会在连接后选择该数据库
261
-
262
- Returns:
263
- 数据库连接对象
264
-
265
- Raises:
266
- ConnectionError: 当获取连接失败时抛出
267
- """
256
+ """从连接池获取数据库连接"""
268
257
  try:
269
- # 只在连续失败次数达到阈值时检查健康状态
270
258
  if self._pool_stats['consecutive_failures'] >= self._pool_stats['max_consecutive_failures']:
271
259
  if not self._check_pool_health():
272
260
  logger.warning('连接池不健康,尝试重新创建')
@@ -282,66 +270,184 @@ class QueryDatas:
282
270
  error_code = e.args[0] if e.args else None
283
271
  if error_code in (2003, 2006, 2013):
284
272
  logger.error('数据库连接错误', {
273
+ '库': db_name,
285
274
  '错误代码': error_code,
286
275
  '错误信息': str(e),
287
- '数据库': db_name
288
276
  })
289
277
  self.pool = self._create_connection_pool(10, 2, 5)
290
278
  self._pool_stats['consecutive_failures'] = 0
291
279
  raise ConnectionError(f'数据库连接错误: {str(e)}')
292
- else:
293
- raise
280
+ raise
294
281
  except Exception as e:
295
282
  logger.error('从连接池获取数据库连接失败', {
283
+ '库': db_name,
296
284
  '错误': str(e),
297
- '数据库': db_name
298
285
  })
299
286
  raise ConnectionError(f'连接数据库失败: {str(e)}')
300
287
 
301
288
  # @_execute_with_retry
302
- def _execute_query(self, sql: str, params: tuple = None, db_name: str = None) -> Optional[List[Dict[str, Any]]]:
289
+ def _execute_query(self, sql: str, params: tuple = None, db_name: str = None,
290
+ fetch_all: bool = True, error_handling: bool = True) -> Optional[Union[List[Dict[str, Any]], Dict[str, Any]]]:
291
+ """执行SQL查询的通用方法"""
292
+ try:
293
+ if sql.upper().startswith('SHOW DATABASES'):
294
+ with closing(self._get_connection()) as connection:
295
+ with closing(connection.cursor()) as cursor:
296
+ cursor.execute(sql, params)
297
+ return cursor.fetchall() if fetch_all else cursor.fetchone()
298
+ else:
299
+ with closing(self._get_connection(db_name)) as connection:
300
+ with closing(connection.cursor()) as cursor:
301
+ cursor.execute(sql, params)
302
+ return cursor.fetchall() if fetch_all else cursor.fetchone()
303
+ except pymysql.OperationalError as e:
304
+ error_code = e.args[0] if e.args else None
305
+ if error_handling:
306
+ if error_code in (1045, 1049): # 访问被拒绝或数据库不存在
307
+ logger.error('数据库访问错误', {
308
+ 'SQL': sql,
309
+ '参数': params,
310
+ '库': db_name,
311
+ '错误代码': error_code,
312
+ '错误信息': str(e)
313
+ })
314
+ else:
315
+ logger.error('数据库操作错误', {
316
+ '库': db_name,
317
+ 'SQL': sql,
318
+ '参数': params,
319
+ '错误代码': error_code,
320
+ '错误信息': str(e)
321
+ })
322
+ return None
323
+ raise
324
+ except Exception as e:
325
+ if error_handling:
326
+ logger.error('执行SQL查询失败', {
327
+ '库': db_name,
328
+ 'SQL': sql,
329
+ '参数': params,
330
+ '错误类型': type(e).__name__,
331
+ '错误信息': str(e)
332
+ })
333
+ return None
334
+ raise
335
+
336
+ def _get_table_info(self, db_name: str, table_name: str, info_type: Literal['columns', 'dtypes', 'exists'] = 'exists') -> Union[bool, List[Dict[str, Any]], List[str]]:
303
337
  """
304
- 执行SQL查询的通用方法。
338
+ 获取表信息的通用方法。
305
339
 
306
340
  Args:
307
- sql: SQL查询语句
308
- params: 查询参数
309
341
  db_name: 数据库名
310
-
342
+ table_name: 表名
343
+ info_type: 信息类型
344
+ - 'exists': 检查表是否存在(默认)
345
+ - 'columns': 获取列名列表
346
+ - 'dtypes': 获取列名和类型
347
+
311
348
  Returns:
312
- 查询结果列表,如果查询失败返回None
349
+ 根据info_type返回不同类型的信息:
350
+ - 'exists': 返回bool,表示表是否存在
351
+ - 'columns': 返回列名列表
352
+ - 'dtypes': 返回列名和类型的列表
313
353
  """
314
354
  try:
315
- with closing(self._get_connection(db_name)) as connection:
316
- with closing(connection.cursor()) as cursor:
317
- cursor.execute(sql, params)
318
- return cursor.fetchall()
355
+ if info_type == 'exists':
356
+ result = self._execute_query("SHOW DATABASES LIKE %s", (db_name,))
357
+ if not result:
358
+ all_dbs = self._execute_query("SHOW DATABASES")
359
+ available_dbs = [db['Database'] for db in all_dbs] if all_dbs else []
360
+ logger.info('数据库不存在', {
361
+ '库': db_name,
362
+ '可用的数据库': available_dbs,
363
+ '可能的原因': '数据库名称错误或没有访问权限'
364
+ })
365
+ return False
366
+
367
+ result = self._execute_query("SHOW TABLES LIKE %s", (table_name,), db_name=db_name)
368
+ if not result:
369
+ all_tables = self._execute_query("SHOW TABLES", db_name=db_name)
370
+ available_tables = [table[f'Tables_in_{db_name}'] for table in all_tables] if all_tables else []
371
+ logger.info('表不存在', {
372
+ '库': db_name,
373
+ '表': table_name,
374
+ '可用的表': available_tables,
375
+ '可能的原因': '表名称错误或没有访问权限'
376
+ })
377
+ return False
378
+ return True
379
+
380
+ elif info_type == 'columns':
381
+ sql = 'SELECT COLUMN_NAME FROM information_schema.columns WHERE table_schema = %s AND table_name = %s'
382
+ result = self._execute_query(sql, (db_name, table_name))
383
+ return [col['COLUMN_NAME'] for col in result] if result else []
384
+
385
+ elif info_type == 'dtypes':
386
+ sql = 'SELECT COLUMN_NAME, COLUMN_TYPE FROM information_schema.columns WHERE table_schema = %s AND table_name = %s'
387
+ return self._execute_query(sql, (db_name, table_name)) or []
388
+
319
389
  except Exception as e:
320
- logger.error('执行SQL查询失败', {
321
- 'SQL': sql,
322
- '参数': params,
323
- '数据库': db_name,
390
+ logger.error('获取表信息失败', {
391
+ '': db_name,
392
+ '': table_name,
393
+ '信息类型': info_type,
324
394
  '错误类型': type(e).__name__,
325
395
  '错误信息': str(e)
326
396
  })
327
- return None
397
+ return [] if info_type != 'exists' else False
328
398
 
329
- def check_condition(self, db_name: str, table_name: str, condition: str, columns: str = '更新时间') -> Optional[List[Dict[str, Any]]]:
330
- """
331
- 按指定条件查询数据库表,返回满足条件的指定字段数据。
399
+ def check_infos(self, db_name: str, table_name: str) -> bool:
400
+ """检查数据库和数据表是否存在"""
401
+ return self._get_table_info(db_name, table_name, 'exists')
402
+
403
+ def _format_columns(self, columns: List[str]) -> str:
404
+ """格式化列名列表为SQL语句"""
405
+ return ', '.join([f'`{col}`' for col in columns])
406
+
407
+ def columns_to_list(self, db_name: str, table_name: str, columns_name: List[str], where: str = None) -> List[Dict[str, Any]]:
408
+ """获取数据表的指定列数据"""
409
+ if not self._get_table_info(db_name, table_name):
410
+ return []
332
411
 
333
- Args:
334
- db_name: 数据库名
335
- table_name: 表名
336
- condition: SQL条件字符串(不含WHERE)
337
- columns: 查询字段字符串或以逗号分隔的字段名,默认'更新时间'
412
+ try:
413
+ existing_columns = self._get_table_info(db_name, table_name, 'columns')
414
+ columns_name = [col for col in columns_name if col in existing_columns]
338
415
 
339
- Returns:
340
- 查询结果列表,如果查询失败返回None
341
- """
342
- if not self.check_infos(db_name, table_name):
343
- return None
416
+ if not columns_name:
417
+ logger.info('未找到匹配的列名', {'库': db_name, '表': table_name, '请求列': columns_name})
418
+ return []
419
+
420
+ sql = f"SELECT {self._format_columns(columns_name)} FROM `{db_name}`.`{table_name}`"
421
+ if where:
422
+ sql += f" WHERE {where}"
344
423
 
424
+ logger.debug('执行列查询', {'库': db_name, '表': table_name, 'SQL': sql})
425
+ return self._execute_query(sql, db_name=db_name) or []
426
+
427
+ except Exception as e:
428
+ logger.error('列查询失败', {'库': db_name, '表': table_name, '列': columns_name, '错误': str(e)})
429
+ return []
430
+
431
+ def dtypes_to_list(self, db_name: str, table_name: str, columns_name: List[str] = None) -> List[Dict[str, Any]]:
432
+ """获取数据表的列名和类型"""
433
+ if not self._get_table_info(db_name, table_name):
434
+ return []
435
+
436
+ try:
437
+ result = self._get_table_info(db_name, table_name, 'dtypes')
438
+ if columns_name:
439
+ columns_name = set(columns_name)
440
+ result = [row for row in result if row['COLUMN_NAME'] in columns_name]
441
+ return result
442
+ except Exception as e:
443
+ logger.error('获取列类型失败', {'库': db_name, '表': table_name, '列': columns_name, '错误': str(e)})
444
+ return []
445
+
446
+ def check_condition(self, db_name: str, table_name: str, condition: str, columns: str = '更新时间') -> Optional[List[Dict[str, Any]]]:
447
+ """按指定条件查询数据库表"""
448
+ if not self._get_table_info(db_name, table_name):
449
+ return None
450
+
345
451
  sql = f"SELECT {columns} FROM `{table_name}` WHERE {condition}"
346
452
  logger.debug('执行SQL查询', {'库': db_name, '表': table_name, 'SQL': sql})
347
453
  return self._execute_query(sql, db_name=db_name)
@@ -598,98 +704,6 @@ class QueryDatas:
598
704
  df[col] = df[col].astype(float)
599
705
  return df
600
706
 
601
- # @_execute_with_retry
602
- def columns_to_list(self, db_name, table_name, columns_name, where: str = None) -> list:
603
- """
604
- 获取数据表的指定列, 支持where条件筛选, 返回列表字典。
605
- :param db_name: 数据库名
606
- :param table_name: 表名
607
- :param columns_name: 需要获取的列名列表
608
- :param where: 可选,SQL条件字符串(不含WHERE)
609
- :return: [{列1:值, 列2:值, ...}, ...]
610
- """
611
- if not self.check_infos(db_name, table_name):
612
- return []
613
-
614
- try:
615
- with closing(self._get_connection(db_name)) as connection:
616
- with closing(connection.cursor()) as cursor:
617
- sql = 'SELECT COLUMN_NAME FROM information_schema.columns WHERE table_schema = %s AND table_name = %s'
618
- cursor.execute(sql, (db_name, table_name))
619
- cols_exist = [col['COLUMN_NAME'] for col in cursor.fetchall()]
620
- columns_name = [item for item in columns_name if item in cols_exist]
621
- if not columns_name:
622
- logger.info('未找到匹配的列名', {'库': db_name, '表': table_name, '请求列': columns_name})
623
- return []
624
- columns_in = ', '.join([f'`{col}`' for col in columns_name])
625
- sql = f"SELECT {columns_in} FROM `{db_name}`.`{table_name}`"
626
- if where:
627
- sql += f" WHERE {where}"
628
- logger.debug('执行列查询', {'库': db_name, '表': table_name, 'SQL': sql})
629
- cursor.execute(sql)
630
- column_values = cursor.fetchall()
631
- return column_values
632
- except Exception as e:
633
- logger.error('列查询失败', {'库': db_name, '表': table_name, '列': columns_name, '错误': str(e)})
634
- return []
635
-
636
- # @_execute_with_retry
637
- def dtypes_to_list(self, db_name, table_name, columns_name=None) -> list:
638
- """
639
- 获取数据表的列名和类型, 支持只返回部分字段类型。
640
- :param db_name: 数据库名
641
- :param table_name: 表名
642
- :param columns_name: 可选,字段名列表,仅返回这些字段的类型
643
- :return: [{'COLUMN_NAME': ..., 'COLUMN_TYPE': ...}, ...]
644
- """
645
- if not self.check_infos(db_name, table_name):
646
- return []
647
-
648
- try:
649
- with closing(self._get_connection(db_name)) as connection:
650
- with closing(connection.cursor()) as cursor:
651
- sql = 'SELECT COLUMN_NAME, COLUMN_TYPE FROM information_schema.columns WHERE table_schema = %s AND table_name = %s'
652
- cursor.execute(sql, (db_name, table_name))
653
- column_name_and_type = cursor.fetchall()
654
- if columns_name:
655
- columns_name = set(columns_name)
656
- column_name_and_type = [row for row in column_name_and_type if row['COLUMN_NAME'] in columns_name]
657
- return column_name_and_type
658
- except Exception as e:
659
- logger.error('获取列类型失败', {'库': db_name, '表': table_name, '列': columns_name, '错误': str(e)})
660
- return []
661
-
662
- # @_execute_with_retry
663
- def check_infos(self, db_name, table_name) -> bool:
664
- """
665
- 检查数据库和数据表是否存在。
666
- :param db_name: 数据库名
667
- :param table_name: 表名
668
- :return: 存在返回True,否则False
669
- """
670
- try:
671
- # 检查数据库是否存在
672
- result = self._execute_query("SHOW DATABASES LIKE %s", (db_name,))
673
- if not result:
674
- logger.info('数据库不存在', {'库': db_name})
675
- return False
676
-
677
- # 检查表是否存在
678
- result = self._execute_query("SHOW TABLES LIKE %s", (table_name,), db_name=db_name)
679
- if not result:
680
- logger.info('表不存在', {'库': db_name, '表': table_name})
681
- return False
682
- return True
683
-
684
- except Exception as e:
685
- logger.error('检查数据库或表失败', {
686
- '库': db_name,
687
- '表': table_name,
688
- '错误类型': type(e).__name__,
689
- '错误信息': str(e)
690
- })
691
- return False
692
-
693
707
  def __enter__(self):
694
708
  """上下文管理器入口"""
695
709
  return self
@@ -772,6 +786,8 @@ class QueryDatas:
772
786
  - 当return_format='list_dict'时,返回列表字典
773
787
  - 如果查询失败,返回空的DataFrame或空列表
774
788
  """
789
+ start_time = time.time()
790
+
775
791
  if not db_name or not table_name:
776
792
  logger.error('数据库名和表名不能为空', {'库': db_name, '表': table_name})
777
793
  return [] if return_format == 'list_dict' else pd.DataFrame()
@@ -786,7 +802,7 @@ class QueryDatas:
786
802
  start_date, end_date = self._validate_date_range(start_date, end_date, db_name, table_name)
787
803
 
788
804
  # 检查数据库和表是否存在
789
- if not self.check_infos(db_name, table_name):
805
+ if not self._get_table_info(db_name, table_name):
790
806
  return [] if return_format == 'list_dict' else pd.DataFrame()
791
807
  try:
792
808
  with closing(self._get_connection(db_name)) as connection:
@@ -863,7 +879,7 @@ class QueryDatas:
863
879
  target_time = 1.0 # 期望每批1秒
864
880
 
865
881
  while offset < total_count:
866
- start_time = time.time()
882
+ _p_time = time.time()
867
883
  # 添加分页参数
868
884
  page_sql = f"{base_sql} LIMIT %s OFFSET %s"
869
885
  page_params = list(params) + [page_size, offset]
@@ -881,7 +897,7 @@ class QueryDatas:
881
897
  else:
882
898
  all_results = pd.concat([all_results, pd.DataFrame(page_results)], ignore_index=True)
883
899
 
884
- duration = time.time() - start_time
900
+ duration = time.time() - _p_time
885
901
  page_size = self._adjust_page_size(duration, page_size, min_size, max_size, target_time)
886
902
  offset += len(page_results)
887
903
  logger.debug('分页查询进度', {
@@ -896,6 +912,21 @@ class QueryDatas:
896
912
 
897
913
  if return_format == 'df' and isinstance(all_results, pd.DataFrame) and not all_results.empty:
898
914
  all_results = self._convert_decimal_columns(all_results)
915
+ logger.info('查询完成', {
916
+ '库': db_name,
917
+ '表': table_name,
918
+ '总记录数': total_count,
919
+ '已获取记录数': len(all_results) if return_format == 'list_dict' else len(all_results.index),
920
+ '查询耗时': f'{time.time() - start_time:.2f}s',
921
+ '查询参数': {
922
+ '开始日期': start_date,
923
+ '结束日期': end_date,
924
+ '日期字段': date_field,
925
+ '限制行数': limit,
926
+ '分页大小': page_size,
927
+ '返回数据格式': return_format,
928
+ }
929
+ })
899
930
  return all_results
900
931
 
901
932
  except Exception as e:
@@ -903,7 +934,15 @@ class QueryDatas:
903
934
  '库': db_name,
904
935
  '表': table_name,
905
936
  '错误类型': type(e).__name__,
906
- '错误信息': str(e)
937
+ '错误信息': str(e),
938
+ '查询参数': {
939
+ '开始日期': start_date,
940
+ '结束日期': end_date,
941
+ '日期字段': date_field,
942
+ '限制行数': limit,
943
+ '分页大小': page_size,
944
+ '返回数据格式': return_format,
945
+ }
907
946
  })
908
947
  return [] if return_format == 'list_dict' else pd.DataFrame()
909
948
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: mdbq
3
- Version: 4.0.9
3
+ Version: 4.0.10
4
4
  Home-page: https://pypi.org/project/mdbq
5
5
  Author: xigua,
6
6
  Author-email: 2587125111@qq.com
@@ -1 +0,0 @@
1
- VERSION = '4.0.9'
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
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