tinybird-cli 5.9.1.dev0__tar.gz → 5.9.1.dev2__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 (48) hide show
  1. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/PKG-INFO +7 -1
  2. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/__cli__.py +2 -2
  3. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/ch_utils/constants.py +1 -1
  4. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/datafile.py +35 -7
  5. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/feedback_manager.py +3 -0
  6. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/sql.py +80 -1
  7. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/sql_template.py +17 -4
  8. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/branch.py +0 -1
  9. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird_cli.egg-info/PKG-INFO +7 -1
  10. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird_cli.egg-info/requires.txt +1 -1
  11. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/setup.cfg +0 -0
  12. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/ch_utils/engine.py +0 -0
  13. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/check_pypi.py +0 -0
  14. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/client.py +0 -0
  15. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/config.py +0 -0
  16. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/connectors.py +0 -0
  17. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/context.py +0 -0
  18. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/datatypes.py +0 -0
  19. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/git_settings.py +0 -0
  20. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/sql_template_fmt.py +0 -0
  21. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/sql_toolset.py +0 -0
  22. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/syncasync.py +0 -0
  23. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli.py +0 -0
  24. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/auth.py +0 -0
  25. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/cicd.py +0 -0
  26. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/cli.py +0 -0
  27. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/common.py +0 -0
  28. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/config.py +0 -0
  29. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/connection.py +0 -0
  30. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/datasource.py +0 -0
  31. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/exceptions.py +0 -0
  32. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/fmt.py +0 -0
  33. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/job.py +0 -0
  34. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/pipe.py +0 -0
  35. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/regions.py +0 -0
  36. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/tag.py +0 -0
  37. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/telemetry.py +0 -0
  38. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/test.py +0 -0
  39. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  40. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  41. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/token.py +0 -0
  42. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/workspace.py +0 -0
  43. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  44. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird/tornado_template.py +0 -0
  45. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  46. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  47. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird_cli.egg-info/entry_points.txt +0 -0
  48. {tinybird-cli-5.9.1.dev0 → tinybird-cli-5.9.1.dev2}/tinybird_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 5.9.1.dev0
3
+ Version: 5.9.1.dev2
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -18,6 +18,12 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
+ 5.9.1.dev2
22
+ ***********
23
+
24
+ - `Changed` Upgrade clickhouse-toolset to 0.32.dev0
25
+ - `Added` new "File not found" error to `tb check` when including files from missing paths.
26
+
21
27
  5.9.0
22
28
  ***********
23
29
 
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://www.tinybird.co/docs/cli/introduction.html'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '5.9.1.dev0'
8
- __revision__ = '9718298'
7
+ __version__ = '5.9.1.dev2'
8
+ __revision__ = '4f458c3'
@@ -6,7 +6,7 @@ SNAPSHOT_WS_NAME = "snapshot"
6
6
  ENABLED_TABLE_FUNCTIONS = {"generateRandom", "null", "numbers", "numbers_mt", "values", "zeros", "zeros_mt"}
7
7
  # there's a workspace limit allowed_table_functions used in the APIs limit in cheriff
8
8
  COPY_ENABLED_TABLE_FUNCTIONS = frozenset(
9
- ["postgresql", "mysql", "mongodb", "url", "azureBlobStorage", "gcs", "iceberg", "s3"]
9
+ ["postgresql", "mysql", "mongodb", "url", "azureBlobStorage", "gcs", "iceberg", "s3", "deltaLake"]
10
10
  )
11
11
 
12
12
  ENABLED_SYSTEM_TABLES = {
@@ -278,7 +278,9 @@ class ParseException(Exception):
278
278
 
279
279
 
280
280
  class IncludeFileNotFoundException(Exception):
281
- pass
281
+ def __init__(self, err: str, lineno: int = -1):
282
+ self.lineno: int = lineno
283
+ super().__init__(err)
282
284
 
283
285
 
284
286
  class ValidationException(Exception):
@@ -307,6 +309,10 @@ def is_shared_datasource(ds_name: str) -> bool:
307
309
  return "." in ds_name
308
310
 
309
311
 
312
+ def format_filename(filename: str, hide_folders: bool = False):
313
+ return os.path.basename(filename) if hide_folders else filename
314
+
315
+
310
316
  class CLIGitRelease:
311
317
  NO_DATAFILES_PATHS = ["vendor/", "tests/", "scripts/", ".diff_tmp/"]
312
318
  DATAFILES_SUFFIXES = [".datasource", ".pipe", ".incl", ".token"]
@@ -783,7 +789,11 @@ class Datafile:
783
789
 
784
790
 
785
791
  def parse_datasource(
786
- filename: str, replace_includes: bool = True, content: Optional[str] = None, skip_eval: bool = False
792
+ filename: str,
793
+ replace_includes: bool = True,
794
+ content: Optional[str] = None,
795
+ skip_eval: bool = False,
796
+ hide_folders: bool = False,
787
797
  ) -> Datafile:
788
798
  basepath = ""
789
799
  if not content:
@@ -793,6 +803,7 @@ def parse_datasource(
793
803
  else:
794
804
  s = content
795
805
 
806
+ filename = format_filename(filename, hide_folders)
796
807
  try:
797
808
  doc = parse(s, "default", basepath, replace_includes=replace_includes, skip_eval=skip_eval)
798
809
  except ParseException as e:
@@ -807,7 +818,11 @@ def parse_datasource(
807
818
 
808
819
 
809
820
  def parse_pipe(
810
- filename: str, replace_includes: bool = True, content: Optional[str] = None, skip_eval: bool = False
821
+ filename: str,
822
+ replace_includes: bool = True,
823
+ content: Optional[str] = None,
824
+ skip_eval: bool = False,
825
+ hide_folders: bool = False,
811
826
  ) -> Datafile:
812
827
  basepath = ""
813
828
  if not content:
@@ -817,6 +832,7 @@ def parse_pipe(
817
832
  else:
818
833
  s = content
819
834
 
835
+ filename = format_filename(filename, hide_folders)
820
836
  try:
821
837
  sql = ""
822
838
  doc = parse(s, basepath=basepath, replace_includes=replace_includes, skip_eval=skip_eval)
@@ -849,13 +865,19 @@ def parse_pipe(
849
865
  raise click.ClickException(
850
866
  FeedbackManager.error_parsing_node_with_unclosed_if(node=e.node, pipe=filename, lineno=e.lineno, sql=e.sql)
851
867
  )
868
+ except IncludeFileNotFoundException as e:
869
+ raise click.ClickException(FeedbackManager.error_not_found_include(filename=e, lineno=e.lineno))
852
870
  except ModuleNotFoundError:
853
871
  pass
854
872
  return doc
855
873
 
856
874
 
857
875
  def parse_token(
858
- filename: str, replace_includes: bool = True, content: Optional[str] = None, skip_eval: bool = False
876
+ filename: str,
877
+ replace_includes: bool = True,
878
+ content: Optional[str] = None,
879
+ skip_eval: bool = False,
880
+ hide_folders: bool = False,
859
881
  ) -> Datafile:
860
882
  if not content:
861
883
  with open(filename) as file:
@@ -864,6 +886,7 @@ def parse_token(
864
886
  else:
865
887
  s = content
866
888
 
889
+ filename = format_filename(filename, hide_folders)
867
890
  try:
868
891
  sql = ""
869
892
  doc = parse(s, basepath=basepath, replace_includes=replace_includes, skip_eval=skip_eval)
@@ -1091,7 +1114,7 @@ def parse(
1091
1114
  StringIO(Template(file.read()).safe_substitute(attrs), newline=None)
1092
1115
  )
1093
1116
  except FileNotFoundError:
1094
- raise IncludeFileNotFoundException(f)
1117
+ raise IncludeFileNotFoundException(f, lineno)
1095
1118
 
1096
1119
  def version(*args: str, **kwargs: Any) -> None:
1097
1120
  if len(args) < 1:
@@ -1279,7 +1302,7 @@ def parse(
1279
1302
  else:
1280
1303
  raise ValidationException(f"Validation error, found {line} in line {str(lineno)}: {str(e)}", lineno=lineno)
1281
1304
  except IncludeFileNotFoundException as e:
1282
- raise e
1305
+ raise IncludeFileNotFoundException(str(e), lineno=lineno)
1283
1306
  except Exception as e:
1284
1307
  traceback.print_tb(e.__traceback__)
1285
1308
  raise ParseException(f"Unexpected error: {e}", lineno=lineno)
@@ -4165,6 +4188,7 @@ async def folder_push(
4165
4188
  check_backfill_required: bool = False,
4166
4189
  use_main: bool = False,
4167
4190
  check_outdated: bool = True,
4191
+ hide_folders: bool = False,
4168
4192
  ): # noqa: C901
4169
4193
  workspaces: List[Dict[str, Any]] = (await tb_client.user_workspaces_and_branches()).get("workspaces", [])
4170
4194
  current_ws: Dict[str, Any] = next(
@@ -4361,8 +4385,12 @@ async def folder_push(
4361
4385
  )
4362
4386
  )
4363
4387
  except Exception as e:
4388
+ filename = (
4389
+ os.path.basename(to_run[name]["filename"]) if hide_folders else to_run[name]["filename"]
4390
+ )
4364
4391
  exception = FeedbackManager.error_push_file_exception(
4365
- filename=to_run[name]["filename"], error=e
4392
+ filename=filename,
4393
+ error=e,
4366
4394
  )
4367
4395
  raise click.ClickException(exception)
4368
4396
  else:
@@ -242,6 +242,9 @@ class FeedbackManager:
242
242
  error_deleted_include = error_message(
243
243
  "Related include file {include_file} was deleted and it's used in {filename}. Delete or remove dependency from {filename}."
244
244
  )
245
+ error_not_found_include = error_message(
246
+ "Included file {filename} at line {lineno} not found. Check if the file exists and the path is correct."
247
+ )
245
248
  error_branch = error_message(
246
249
  "Branch {branch} not found. use 'tb branch ls' to list your Branches, make sure you are authenticated using the right workspace token"
247
250
  )
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import re
2
3
  import string
3
4
  from collections import namedtuple
@@ -401,10 +402,84 @@ def parse_table_structure(schema: str) -> List[Dict[str, Any]]:
401
402
  [{'name': 'a', 'type': 'String', 'codec': None, 'default_value': None, 'jsonpath': None, 'nullable': False, 'normalized_name': 'a'}]
402
403
  >>> parse_table_structure('`index` String, INDEX index_name a TYPE set(100, 1) GRANULARITY 100')
403
404
  [{'name': 'index', 'type': 'String', 'codec': None, 'default_value': None, 'jsonpath': None, 'nullable': False, 'normalized_name': 'index'}]
405
+ >>> parse_table_structure('`a2` String `json:$.a--2`, `a3` String `json:$.a3`\\n')
406
+ [{'name': 'a2', 'type': 'String', 'codec': None, 'default_value': None, 'jsonpath': '$.a--2', 'nullable': False, 'normalized_name': 'a2'}, {'name': 'a3', 'type': 'String', 'codec': None, 'default_value': None, 'jsonpath': '$.a3', 'nullable': False, 'normalized_name': 'a3'}]
404
407
  """
405
408
  return _parse_table_structure(schema)
406
409
 
407
410
 
411
+ def clean_comments(schema_to_clean: str) -> str:
412
+ """Remove the comments from the schema
413
+ if the comments are between backticks, they will not be removed
414
+ >>> clean_comments(None) is None
415
+ True
416
+ >>> clean_comments('')
417
+ ''
418
+ >>> clean_comments(' ')
419
+ ''
420
+ >>> clean_comments('\\n')
421
+ ''
422
+ >>> clean_comments('\\n\\n\\n\\n')
423
+ ''
424
+ >>> clean_comments('c Float32')
425
+ 'c Float32'
426
+ >>> clean_comments('c Float32\\n')
427
+ 'c Float32'
428
+ >>> clean_comments('c Float32\\n--this is a comment')
429
+ 'c Float32'
430
+ >>> clean_comments('c Float32\\n--this is a comment\\n')
431
+ 'c Float32'
432
+ >>> clean_comments('c Float32\\t-- this is a comment\\t\\n')
433
+ 'c Float32'
434
+ >>> clean_comments('c Float32\\n--this is a comment\\r\\n')
435
+ 'c Float32'
436
+ >>> clean_comments('c Float32\\n--this is a comment\\n--this is a comment2\\n')
437
+ 'c Float32'
438
+ >>> clean_comments('c Float32\\n--this is a ```comment\\n')
439
+ 'c Float32'
440
+ >>> clean_comments('c Float32\\n--this is a ```comment\\n')
441
+ 'c Float32'
442
+ >>> clean_comments('c Float32, -- comment\\nd Float32 -- comment2')
443
+ 'c Float32,\\nd Float32'
444
+ >>> clean_comments('c Float32, -- comment\\n -- comment \\nd Float32 -- comment2')
445
+ 'c Float32,\\nd Float32'
446
+ >>> clean_comments('c Float32 `json:$.aa--aa`\\n--this is a ```comment\\n')
447
+ 'c Float32 `json:$.aa--aa`'
448
+ >>> clean_comments('c Float32 `json:$.cc--cc`\\nd Float32 `json:$.dd--dd`\\n--this is a ```comment\\n')
449
+ 'c Float32 `json:$.cc--cc`\\nd Float32 `json:$.dd--dd`'
450
+ >>> clean_comments('c--c Float32 `json:$.cc--cc`\\n')
451
+ 'c'
452
+ >>> clean_comments('`c--c` Float32 `json:$.cc--cc`\\n')
453
+ '`c'
454
+ """
455
+
456
+ def clean_line_comments(line: str) -> str:
457
+ if not line:
458
+ return line
459
+ i = 0
460
+ inside_json_path = False
461
+ while i < len(line):
462
+ if i + 1 < len(line) and line[i] == "-" and line[i + 1] == "-" and not inside_json_path:
463
+ return line[:i].strip()
464
+
465
+ if not inside_json_path and line[i:].startswith("`json:"):
466
+ inside_json_path = True
467
+ elif inside_json_path and line[i] == "`":
468
+ inside_json_path = False
469
+ i += 1
470
+ return line
471
+
472
+ if schema_to_clean is None:
473
+ return schema_to_clean
474
+
475
+ cleaned_schema = ""
476
+ for line in schema_to_clean.splitlines():
477
+ cleaned_line = clean_line_comments(line)
478
+ if cleaned_line:
479
+ cleaned_schema += cleaned_line + "\n"
480
+ return cleaned_schema.strip()
481
+
482
+
408
483
  SyntaxExpr = namedtuple("SyntaxExpr", ["name", "regex"])
409
484
 
410
485
  NULL = SyntaxExpr("NULL", re.compile(r"\s+NULL([^a-z0-9_]|$)", re.IGNORECASE))
@@ -426,7 +501,11 @@ REGEX_COMMENT = re.compile(r"\-\-[^\n\r]*[\n\r]")
426
501
  def _parse_table_structure(schema: str) -> List[Dict[str, Any]]: # noqa: C901
427
502
  # CH syntax from https://clickhouse.com/docs/en/sql-reference/statements/create/table/
428
503
  # name1 [type1] [NULL|NOT NULL] [DEFAULT|MATERIALIZED|ALIAS expr1] [compression_codec] [TTL expr1]
429
- schema = REGEX_COMMENT.sub(" ", schema + "\n").strip()
504
+ try:
505
+ schema = clean_comments(schema + "\n")
506
+ except Exception as e:
507
+ logging.exception(f"Error cleaning comments: {e}")
508
+ schema = REGEX_COMMENT.sub(" ", schema + "\n").strip()
430
509
 
431
510
  if REGEX_WHITESPACE.fullmatch(schema):
432
511
  return []
@@ -750,6 +750,20 @@ def _parse_datetime(date_string, date_format, backup_date_format=None):
750
750
 
751
751
 
752
752
  def json_type(x, default=None):
753
+ """
754
+ >>> json_type(None, '[]')
755
+ []
756
+ >>> json_type(None)
757
+ {}
758
+ >>> json_type('{"a": 1}')
759
+ {'a': 1}
760
+ >>> json_type('[{"a": 1}]')
761
+ [{'a': 1}]
762
+ >>> json_type({"a": 1})
763
+ {'a': 1}
764
+ >>> json_type([{"a": 1}])
765
+ [{'a': 1}]
766
+ """
753
767
  if isinstance(x, Placeholder):
754
768
  if default:
755
769
  x = default
@@ -764,15 +778,14 @@ def json_type(x, default=None):
764
778
  x = "{}"
765
779
 
766
780
  value = "" # used for exception message
767
- if isinstance(x, str):
781
+ if isinstance(x, (str, bytes, bytearray)):
768
782
  if len(x) > 16:
769
783
  value = x[:16] + "..."
770
784
  else:
771
785
  value = x
772
786
 
773
- parsed = loads(x)
774
- x = parsed
775
-
787
+ parsed = loads(x)
788
+ x = parsed
776
789
  except Exception as e:
777
790
  msg = f"Error parsing JSON: '{value}' - {str(e)}"
778
791
  raise SQLTemplateException(msg)
@@ -47,7 +47,6 @@ def release() -> None:
47
47
  @coro
48
48
  async def release_ls() -> None:
49
49
  """List current available Releases in the Workspace"""
50
- click.echo(FeedbackManager.warning_deprecated_releases())
51
50
  config = CLIConfig.get_project_config()
52
51
  _ = await try_update_config_with_remote(config, only_if_needed=True)
53
52
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 5.9.1.dev0
3
+ Version: 5.9.1.dev2
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://www.tinybird.co/docs/cli/introduction.html
6
6
  Author: Tinybird
@@ -18,6 +18,12 @@ The Tinybird command-line tool allows you to use all the Tinybird functionality
18
18
  Changelog
19
19
  ----------
20
20
 
21
+ 5.9.1.dev2
22
+ ***********
23
+
24
+ - `Changed` Upgrade clickhouse-toolset to 0.32.dev0
25
+ - `Added` new "File not found" error to `tb check` when including files from missing paths.
26
+
21
27
  5.9.0
22
28
  ***********
23
29
 
@@ -1,5 +1,5 @@
1
1
  click<8.2,>=8.1.6
2
- clickhouse-toolset==0.31.dev1
2
+ clickhouse-toolset==0.32.dev0
3
3
  colorama==0.4.6
4
4
  cryptography>=3.4.8
5
5
  croniter==1.3.8