tinybird-cli 3.2.1.dev5__tar.gz → 3.2.1.dev7__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-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/PKG-INFO +12 -3
  2. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/__cli__.py +2 -2
  3. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/client.py +6 -0
  4. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/connector_settings.py +5 -11
  5. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/data_connectors/credentials.py +1 -1
  6. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/datafile.py +3 -0
  7. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/feedback_manager.py +11 -0
  8. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/common.py +16 -6
  9. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/connection.py +139 -0
  10. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird_cli.egg-info/PKG-INFO +12 -3
  11. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/setup.cfg +0 -0
  12. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/ch_utils/constants.py +0 -0
  13. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/ch_utils/engine.py +0 -0
  14. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/check_pypi.py +0 -0
  15. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/config.py +0 -0
  16. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/connectors.py +0 -0
  17. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/context.py +0 -0
  18. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/datatypes.py +0 -0
  19. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/git_settings.py +0 -0
  20. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/sql.py +0 -0
  21. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/sql_template.py +0 -0
  22. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/sql_template_fmt.py +0 -0
  23. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/sql_toolset.py +0 -0
  24. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/syncasync.py +0 -0
  25. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli.py +0 -0
  26. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/auth.py +0 -0
  27. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/branch.py +0 -0
  28. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/cicd.py +0 -0
  29. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/cli.py +0 -0
  30. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/config.py +0 -0
  31. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/datasource.py +0 -0
  32. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/exceptions.py +0 -0
  33. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/job.py +0 -0
  34. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/pipe.py +0 -0
  35. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/regions.py +0 -0
  36. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/telemetry.py +0 -0
  37. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/test.py +0 -0
  38. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/tinyunit/tinyunit.py +0 -0
  39. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/tinyunit/tinyunit_lib.py +0 -0
  40. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/token.py +0 -0
  41. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/workspace.py +0 -0
  42. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tb_cli_modules/workspace_members.py +0 -0
  43. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird/tornado_template.py +0 -0
  44. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird_cli.egg-info/SOURCES.txt +0 -0
  45. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird_cli.egg-info/dependency_links.txt +0 -0
  46. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird_cli.egg-info/entry_points.txt +0 -0
  47. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/tinybird_cli.egg-info/requires.txt +0 -0
  48. {tinybird-cli-3.2.1.dev5 → tinybird-cli-3.2.1.dev7}/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: 3.2.1.dev5
3
+ Version: 3.2.1.dev7
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://docs.tinybird.co/cli.html
6
6
  Author: Tinybird
@@ -13,18 +13,27 @@ Provides-Extra: snowflake
13
13
  Tinybird CLI
14
14
  ============
15
15
 
16
- The Tinybird command-line tool allows you to use all the Tinybird functionality directly from the command line. Additionally, it includes several functions to create and manage data projects easily.
16
+ The Tinybird command-line tool allows you to use all the Tinybird functionality directly from the command line. Additionally it includes several functions to create and manage data projects easily.
17
17
 
18
18
  Changelog
19
19
 
20
20
  ---------
21
21
 
22
+ 3.2.1.dev7
23
+ ************
24
+
25
+ - `Added` `bi_stats`, `bi_stats_rt` and `endpoint_errors` to the list of service data sources that the CLI ignore when looking for dependencies of pipes being pushed.
26
+
27
+ 3.2.1.dev6
28
+ ************
29
+
30
+ - `Added` S3 Iam Role connection creation
31
+
22
32
  3.2.1.dev5
23
33
  ************
24
34
 
25
35
  - `Fixed` Homepage URL in PyPI
26
36
 
27
-
28
37
  3.2.1.dev4
29
38
  ************
30
39
 
@@ -4,5 +4,5 @@ __description__ = 'Tinybird Command Line Tool'
4
4
  __url__ = 'https://docs.tinybird.co/cli.html'
5
5
  __author__ = 'Tinybird'
6
6
  __author_email__ = 'support@tinybird.co'
7
- __version__ = '3.2.1.dev5'
8
- __revision__ = '0395b82'
7
+ __version__ = '3.2.1.dev7'
8
+ __revision__ = '4224631'
@@ -991,6 +991,12 @@ class TinyB(object):
991
991
  response = await self._req(f"/v0/connectors/snowflake/warehouses?{urlencode(params)}", method="POST", data="")
992
992
  return response["warehouses"]
993
993
 
994
+ async def get_s3_trust_policy(self) -> Dict[str, Any]:
995
+ return await self._req("/v0/integrations/s3/policies/trust-policy")
996
+
997
+ async def get_s3_access_policy(self) -> Dict[str, Any]:
998
+ return await self._req("/v0/integrations/s3/policies/access-policy")
999
+
994
1000
  async def sql_get_format(self, sql: str, with_clickhouse_format: bool = False) -> str:
995
1001
  try:
996
1002
  if with_clickhouse_format:
@@ -1,19 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- import logging
4
3
  from enum import Enum
5
4
  from typing import List, Optional, Union
6
5
 
7
6
  from pydantic import BaseModel
8
7
 
9
- from tinybird.data_connectors.credentials import (
10
- GCSConnectorCredentials,
11
- S3ConnectorCredentials,
12
- S3IAMRoleConnectorCredentials,
13
- )
8
+ from tinybird.data_connectors.credentials import GCSConnectorCredentials, IAMRoleAWSCredentials, S3ConnectorCredentials
14
9
 
15
- logger = logging.getLogger(__name__)
16
- logger.setLevel(logging.INFO)
10
+ GCS_BASE_URL = "https://storage.googleapis.com"
17
11
 
18
12
 
19
13
  class DataConnectorType(str, Enum):
@@ -80,8 +74,8 @@ class S3IAMConnectorSetting(DataConnectorSetting):
80
74
  endpoint_url: Optional[str] = None
81
75
 
82
76
  @property
83
- def credentials(self) -> S3IAMRoleConnectorCredentials:
84
- return S3IAMRoleConnectorCredentials(
77
+ def credentials(self) -> IAMRoleAWSCredentials:
78
+ return IAMRoleAWSCredentials(
85
79
  role_arn=self.s3_iamrole_arn,
86
80
  external_id=self.s3_iamrole_external_id,
87
81
  region=self.s3_iamrole_region,
@@ -116,7 +110,7 @@ class BigQueryConnectorSetting(DataConnectorSetting):
116
110
  class GCSHmacConnectorSetting(DataConnectorSetting):
117
111
  gcs_hmac_access_id: str
118
112
  gcs_hmac_secret: str
119
- endpoint_url: Optional[str] = None
113
+ endpoint_url: str = GCS_BASE_URL
120
114
 
121
115
  @property
122
116
  def credentials(self) -> GCSConnectorCredentials:
@@ -14,7 +14,7 @@ class S3ConnectorCredentials(ConnectorCredentials):
14
14
 
15
15
 
16
16
  @dataclasses.dataclass(frozen=True)
17
- class S3IAMRoleConnectorCredentials(ConnectorCredentials):
17
+ class IAMRoleAWSCredentials(ConnectorCredentials):
18
18
  role_arn: str
19
19
  external_id: str
20
20
  region: str
@@ -81,6 +81,9 @@ INTERNAL_TABLES: Tuple[str, ...] = (
81
81
  "data_connectors_log",
82
82
  "kafka_ops_log",
83
83
  "datasources_storage",
84
+ "endpoint_errors",
85
+ "bi_stats_rt",
86
+ "bi_stats",
84
87
  )
85
88
 
86
89
 
@@ -196,6 +196,7 @@ class FeedbackManager:
196
196
  )
197
197
  error_connection_does_not_exists = error_message("Connection {connection} does not exist")
198
198
  error_connection_create = error_message("Connection {connection_name} could not be created: {error}")
199
+ error_connection_integration_not_available = error_message("Connection could not be created: {error}")
199
200
  error_workspace = error_message("Workspace {workspace} not found. use 'tb workspace ls' to list your workspaces")
200
201
  error_branch = error_message(
201
202
  "Branch {branch} not found. use 'tb branch ls' to list your Branches, make sure you are authenticated using the right workspace token"
@@ -360,6 +361,15 @@ class FeedbackManager:
360
361
  ** Assign it the role "BigQuery Data Viewer"
361
362
  Ready? """
362
363
  )
364
+
365
+ prompt_s3_iamrole_connection_login_aws = prompt_message("""[1] Log into your AWS Console\n\n""")
366
+ prompt_s3_iamrole_connection_policy = prompt_message(
367
+ """\n[2] Go to IAM > Policies. Create a new policy with the following permissions. Please, replace <bucket> with your bucket name:\n\n{access_policy}\n\n(The policy has been copied to your clipboard)\n\n"""
368
+ )
369
+ prompt_s3_iamrole_connection_role = prompt_message(
370
+ """\n[3] Go to IAM > Roles. Create a new IAM Role using the following custom trust policy and attach the access policy you just created in the previous step:\n\n{trust_policy}\n\n(The policy has been copied to your clipboard)\n\n"""
371
+ )
372
+
363
373
  prompt_init_git_release_pull = prompt_message(
364
374
  "❓ Download the Data Project to continue, otherwise you can't initialize Workspace with Git. Execute '{pull_command}'?"
365
375
  )
@@ -390,6 +400,7 @@ Ready? """
390
400
  "Connection {name} already exists. Use 'tb connection ls' to list your connections"
391
401
  )
392
402
  info_creating_kafka_connection = info_message("** Creating new Kafka connection '{connection_name}'")
403
+ info_creating_s3_iamrole_connection = info_message("** Creating new S3 IAM Role connection '{connection_name}'")
393
404
 
394
405
  warning_remove_oldest_rollback = warning_message(
395
406
  "[WARNING] Will try to remove oldest rollback Release before promoting to live Release {semver}."
@@ -1147,15 +1147,19 @@ async def print_branch_regression_tests_summary(client, job_id, host, response=N
1147
1147
  step["run"]["metrics_summary"].get("passed", 0),
1148
1148
  step["run"]["metrics_summary"].get("failed", 0),
1149
1149
  format_metric(
1150
- step["run"]["metrics_timing"]["mean response time"][2]
1151
- if "mean response time" in step["run"]["metrics_timing"]
1152
- else 0.0,
1150
+ (
1151
+ step["run"]["metrics_timing"]["mean response time"][2]
1152
+ if "mean response time" in step["run"]["metrics_timing"]
1153
+ else 0.0
1154
+ ),
1153
1155
  is_percentage=True,
1154
1156
  ),
1155
1157
  format_metric(
1156
- step["run"]["metrics_timing"]["mean read bytes"][2]
1157
- if "mean read bytes" in step["run"]["metrics_timing"]
1158
- else 0.0,
1158
+ (
1159
+ step["run"]["metrics_timing"]["mean read bytes"][2]
1160
+ if "mean read bytes" in step["run"]["metrics_timing"]
1161
+ else 0.0
1162
+ ),
1159
1163
  is_percentage=True,
1160
1164
  ),
1161
1165
  ]
@@ -1622,6 +1626,12 @@ class ConnectionReplacements:
1622
1626
  "secret": "s3_secret_access_key",
1623
1627
  "region": "s3_region",
1624
1628
  },
1629
+ "s3_iamrole": {
1630
+ "service": "service",
1631
+ "connection_name": "name",
1632
+ "role_arn": "s3_iamrole_arn",
1633
+ "region": "s3_iamrole_region",
1634
+ },
1625
1635
  "gcs_hmac": {
1626
1636
  "service": "service",
1627
1637
  "connection_name": "name",
@@ -3,12 +3,15 @@
3
3
  # - If it makes sense and only when strictly necessary, you can create utility functions in this file.
4
4
  # - But please, **do not** interleave utility functions and command definitions.
5
5
 
6
+ import json
6
7
  import os
8
+ import sys
7
9
  from os import getcwd
8
10
  from pathlib import Path
9
11
  from typing import Any, Dict, Optional
10
12
 
11
13
  import click
14
+ import pyperclip
12
15
  from click import Context
13
16
 
14
17
  from tinybird.client import DoesNotExistException, TinyB
@@ -620,3 +623,139 @@ async def connection_create_gcs(
620
623
  """
621
624
  )
622
625
  click.echo(FeedbackManager.success_connection_file_created(name=conn_file_name))
626
+
627
+
628
+ @connection_create.command(
629
+ name="s3_iamrole", short_help="Creates a AWS S3 connection using IAM role authentication", hidden=True
630
+ )
631
+ @click.option("--connection-name", default=None, help="The name of the connection to identify it in Tinybird")
632
+ @click.option("--role-arn", default=None, help="The ARN of the IAM role to use for the connection")
633
+ @click.option("--region", default=None, help="The Amazon S3 region where the bucket is located")
634
+ @click.option(
635
+ "--no-validate", is_flag=True, default=False, help="Do not validate S3 permissions during connection creation"
636
+ )
637
+ @click.pass_context
638
+ @coro
639
+ async def connection_create_s3_iamrole(
640
+ ctx: Context,
641
+ connection_name: Optional[str] = "",
642
+ role_arn: Optional[str] = "",
643
+ region: Optional[str] = "",
644
+ no_validate: Optional[bool] = False,
645
+ ) -> None:
646
+ """
647
+ Creates a S3 connection using IAM role authentication in the current workspace
648
+
649
+ \b
650
+ $ tb connection create s3_iamrole
651
+ """
652
+
653
+ obj: Dict[str, Any] = ctx.ensure_object(dict)
654
+ client: TinyB = obj["client"]
655
+
656
+ async def get_s3_policies():
657
+ s3_access_policy: Dict[str, Any] = {}
658
+ try:
659
+ s3_access_policy = await client.get_s3_access_policy()
660
+ if not len(s3_access_policy) > 0:
661
+ raise Exception("S3 Integration not supported in this region")
662
+ except Exception as e:
663
+ raise CLIConnectionException(FeedbackManager.error_connection_integration_not_available(error=str(e)))
664
+
665
+ s3_trust_policy: Dict[str, Any] = {}
666
+ try:
667
+ s3_trust_policy = await client.get_s3_trust_policy()
668
+ if not len(s3_trust_policy) > 0:
669
+ raise Exception("S3 Integration not supported in this region")
670
+ except Exception as e:
671
+ raise CLIConnectionException(FeedbackManager.error_connection_integration_not_available(error=str(e)))
672
+
673
+ return json.dumps(s3_access_policy, indent=4), json.dumps(s3_trust_policy, indent=4)
674
+
675
+ async def validate_s3_integration(role_arn: Optional[str], region: Optional[str]):
676
+ if no_validate is False:
677
+ access_policy, trust_policy = await get_s3_policies()
678
+
679
+ if not role_arn:
680
+ if not click.confirm(
681
+ FeedbackManager.prompt_s3_iamrole_connection_login_aws(),
682
+ show_default=False,
683
+ prompt_suffix="Press y to continue:",
684
+ ):
685
+ sys.exit(1)
686
+
687
+ pyperclip.copy(access_policy)
688
+ if not click.confirm(
689
+ FeedbackManager.prompt_s3_iamrole_connection_policy(access_policy=access_policy),
690
+ show_default=False,
691
+ prompt_suffix="Press y to continue:",
692
+ ):
693
+ sys.exit(1)
694
+
695
+ pyperclip.copy(trust_policy)
696
+ if not click.confirm(
697
+ FeedbackManager.prompt_s3_iamrole_connection_role(trust_policy=trust_policy),
698
+ show_default=False,
699
+ prompt_suffix="Press y to continue:",
700
+ ):
701
+ sys.exit(1)
702
+
703
+ if not role_arn:
704
+ role_arn = click.prompt("Enter the ARN of the role you just created")
705
+ validate_string_connector_param("Role ARN", role_arn)
706
+
707
+ if not region:
708
+ region = click.prompt("Enter the region where the bucket is located")
709
+ validate_string_connector_param("Region", region)
710
+
711
+ return role_arn, region
712
+
713
+ async def validate_connection_name(connection_name: Optional[str]) -> str:
714
+ if connection_name and no_validate is False:
715
+ if await client.get_connector(connection_name) is not None:
716
+ raise CLIConnectionException(FeedbackManager.info_connection_already_exists(name=connection_name))
717
+ else:
718
+ while not connection_name:
719
+ connection_name = click.prompt("Enter the name for this connection", default=None, show_default=False)
720
+ assert isinstance(connection_name, str)
721
+
722
+ if no_validate is False:
723
+ if await client.get_connector(connection_name) is not None:
724
+ click.echo(FeedbackManager.info_connection_already_exists(name=connection_name))
725
+ connection_name = None
726
+ assert isinstance(connection_name, str)
727
+ return connection_name
728
+
729
+ async def create_connection(connection_name, role_arn, region) -> None:
730
+ conn_file_name = f"{connection_name}.connection"
731
+ conn_file_path = Path(getcwd(), conn_file_name)
732
+ service = DataConnectors.AMAZON_S3_IAMROLE
733
+
734
+ if os.path.isfile(conn_file_path):
735
+ raise CLIConnectionException(FeedbackManager.error_connection_file_already_exists(name=conn_file_name))
736
+
737
+ click.echo(FeedbackManager.info_creating_s3_iamrole_connection(connection_name=connection_name))
738
+
739
+ params = ConnectionReplacements.map_api_params_from_prompt_params(
740
+ service, connection_name=connection_name, role_arn=role_arn, region=region
741
+ )
742
+
743
+ click.echo("** Creating connection...")
744
+ try:
745
+ _ = await client.connection_create(params)
746
+ except Exception as e:
747
+ raise CLIConnectionException(
748
+ FeedbackManager.error_connection_create(connection_name=connection_name, error=str(e))
749
+ )
750
+
751
+ with open(conn_file_path, "w") as f:
752
+ f.write(
753
+ f"""TYPE {service}
754
+
755
+ """
756
+ )
757
+ click.echo(FeedbackManager.success_connection_file_created(name=conn_file_name))
758
+
759
+ role_arn, region = await validate_s3_integration(role_arn, region)
760
+ connection_name = await validate_connection_name(connection_name)
761
+ await create_connection(connection_name, role_arn, region)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tinybird-cli
3
- Version: 3.2.1.dev5
3
+ Version: 3.2.1.dev7
4
4
  Summary: Tinybird Command Line Tool
5
5
  Home-page: https://docs.tinybird.co/cli.html
6
6
  Author: Tinybird
@@ -13,18 +13,27 @@ Provides-Extra: snowflake
13
13
  Tinybird CLI
14
14
  ============
15
15
 
16
- The Tinybird command-line tool allows you to use all the Tinybird functionality directly from the command line. Additionally, it includes several functions to create and manage data projects easily.
16
+ The Tinybird command-line tool allows you to use all the Tinybird functionality directly from the command line. Additionally it includes several functions to create and manage data projects easily.
17
17
 
18
18
  Changelog
19
19
 
20
20
  ---------
21
21
 
22
+ 3.2.1.dev7
23
+ ************
24
+
25
+ - `Added` `bi_stats`, `bi_stats_rt` and `endpoint_errors` to the list of service data sources that the CLI ignore when looking for dependencies of pipes being pushed.
26
+
27
+ 3.2.1.dev6
28
+ ************
29
+
30
+ - `Added` S3 Iam Role connection creation
31
+
22
32
  3.2.1.dev5
23
33
  ************
24
34
 
25
35
  - `Fixed` Homepage URL in PyPI
26
36
 
27
-
28
37
  3.2.1.dev4
29
38
  ************
30
39