pymammotion 0.4.0a2__py3-none-any.whl → 0.5.51__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 pymammotion might be problematic. Click here for more details.

Files changed (133) hide show
  1. pymammotion/__init__.py +5 -4
  2. pymammotion/aliyun/client.py +235 -0
  3. pymammotion/aliyun/cloud_gateway.py +312 -64
  4. pymammotion/aliyun/model/aep_response.py +1 -2
  5. pymammotion/aliyun/model/dev_by_account_response.py +170 -23
  6. pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
  7. pymammotion/aliyun/model/regions_response.py +3 -3
  8. pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
  9. pymammotion/aliyun/model/thing_response.py +12 -0
  10. pymammotion/aliyun/regions.py +62 -0
  11. pymammotion/aliyun/tea/core.py +297 -0
  12. pymammotion/bluetooth/ble.py +7 -9
  13. pymammotion/bluetooth/ble_message.py +10 -14
  14. pymammotion/const.py +3 -0
  15. pymammotion/data/model/__init__.py +1 -2
  16. pymammotion/data/model/device.py +95 -27
  17. pymammotion/data/model/device_config.py +4 -4
  18. pymammotion/data/model/device_info.py +35 -0
  19. pymammotion/data/model/device_limits.py +10 -10
  20. pymammotion/data/model/enums.py +12 -2
  21. pymammotion/data/model/errors.py +12 -0
  22. pymammotion/data/model/events.py +14 -0
  23. pymammotion/data/model/generate_geojson.py +521 -0
  24. pymammotion/data/model/generate_route_information.py +2 -2
  25. pymammotion/data/model/hash_list.py +370 -57
  26. pymammotion/data/model/location.py +4 -4
  27. pymammotion/data/model/mowing_modes.py +17 -1
  28. pymammotion/data/model/raw_data.py +2 -10
  29. pymammotion/data/model/region_data.py +10 -11
  30. pymammotion/data/model/report_info.py +31 -5
  31. pymammotion/data/model/work.py +27 -0
  32. pymammotion/data/mower_state_manager.py +316 -0
  33. pymammotion/data/mqtt/event.py +73 -28
  34. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  35. pymammotion/data/mqtt/properties.py +93 -78
  36. pymammotion/data/mqtt/status.py +18 -17
  37. pymammotion/event/event.py +27 -6
  38. pymammotion/homeassistant/__init__.py +3 -0
  39. pymammotion/homeassistant/mower_api.py +484 -0
  40. pymammotion/homeassistant/rtk_api.py +54 -0
  41. pymammotion/http/encryption.py +5 -6
  42. pymammotion/http/http.py +574 -28
  43. pymammotion/http/model/__init__.py +0 -0
  44. pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
  45. pymammotion/http/model/http.py +129 -4
  46. pymammotion/http/model/response_factory.py +61 -0
  47. pymammotion/http/model/rtk.py +16 -0
  48. pymammotion/mammotion/commands/abstract_message.py +7 -5
  49. pymammotion/mammotion/commands/mammotion_command.py +30 -1
  50. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  51. pymammotion/mammotion/commands/messages/driver.py +61 -29
  52. pymammotion/mammotion/commands/messages/media.py +68 -15
  53. pymammotion/mammotion/commands/messages/navigation.py +61 -25
  54. pymammotion/mammotion/commands/messages/network.py +17 -23
  55. pymammotion/mammotion/commands/messages/ota.py +18 -18
  56. pymammotion/mammotion/commands/messages/system.py +32 -49
  57. pymammotion/mammotion/commands/messages/video.py +15 -16
  58. pymammotion/mammotion/devices/__init__.py +27 -3
  59. pymammotion/mammotion/devices/base.py +40 -131
  60. pymammotion/mammotion/devices/mammotion.py +436 -201
  61. pymammotion/mammotion/devices/mammotion_bluetooth.py +57 -47
  62. pymammotion/mammotion/devices/mammotion_cloud.py +134 -105
  63. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  64. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  65. pymammotion/mammotion/devices/managers/managers.py +81 -0
  66. pymammotion/mammotion/devices/mower_device.py +124 -0
  67. pymammotion/mammotion/devices/mower_manager.py +107 -0
  68. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  69. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  70. pymammotion/mammotion/devices/rtk_device.py +50 -0
  71. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  72. pymammotion/mqtt/__init__.py +2 -1
  73. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  74. pymammotion/mqtt/linkkit/__init__.py +5 -0
  75. pymammotion/mqtt/linkkit/h2client.py +585 -0
  76. pymammotion/mqtt/linkkit/linkkit.py +3023 -0
  77. pymammotion/mqtt/mammotion_mqtt.py +176 -169
  78. pymammotion/mqtt/mqtt_models.py +66 -0
  79. pymammotion/proto/__init__.py +4839 -4
  80. pymammotion/proto/basestation.proto +8 -0
  81. pymammotion/proto/basestation_pb2.py +11 -9
  82. pymammotion/proto/basestation_pb2.pyi +16 -2
  83. pymammotion/proto/dev_net.proto +79 -55
  84. pymammotion/proto/dev_net_pb2.py +60 -56
  85. pymammotion/proto/dev_net_pb2.pyi +49 -6
  86. pymammotion/proto/luba_msg.proto +2 -1
  87. pymammotion/proto/luba_msg_pb2.py +6 -6
  88. pymammotion/proto/luba_msg_pb2.pyi +1 -0
  89. pymammotion/proto/luba_mul.proto +62 -1
  90. pymammotion/proto/luba_mul_pb2.py +38 -22
  91. pymammotion/proto/luba_mul_pb2.pyi +94 -7
  92. pymammotion/proto/mctrl_driver.proto +44 -4
  93. pymammotion/proto/mctrl_driver_pb2.py +26 -14
  94. pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
  95. pymammotion/proto/mctrl_nav.proto +93 -52
  96. pymammotion/proto/mctrl_nav_pb2.py +75 -67
  97. pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
  98. pymammotion/proto/mctrl_ota.proto +40 -2
  99. pymammotion/proto/mctrl_ota_pb2.py +23 -13
  100. pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
  101. pymammotion/proto/mctrl_pept.proto +8 -3
  102. pymammotion/proto/mctrl_pept_pb2.py +8 -6
  103. pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  104. pymammotion/proto/mctrl_sys.proto +325 -86
  105. pymammotion/proto/mctrl_sys_pb2.py +162 -98
  106. pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
  107. pymammotion/proto/message_pool.py +3 -0
  108. pymammotion/proto/py.typed +0 -0
  109. pymammotion/utility/constant/device_constant.py +29 -5
  110. pymammotion/utility/datatype_converter.py +13 -12
  111. pymammotion/utility/device_config.py +522 -130
  112. pymammotion/utility/device_type.py +218 -21
  113. pymammotion/utility/map.py +238 -51
  114. pymammotion/utility/mur_mur_hash.py +159 -0
  115. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/METADATA +26 -31
  116. pymammotion-0.5.51.dist-info/RECORD +152 -0
  117. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
  118. pymammotion/aliyun/cloud_service.py +0 -65
  119. pymammotion/data/model/plan.py +0 -58
  120. pymammotion/data/state_manager.py +0 -129
  121. pymammotion/proto/basestation.py +0 -59
  122. pymammotion/proto/common.py +0 -12
  123. pymammotion/proto/dev_net.py +0 -381
  124. pymammotion/proto/luba_msg.py +0 -81
  125. pymammotion/proto/luba_mul.py +0 -76
  126. pymammotion/proto/mctrl_driver.py +0 -100
  127. pymammotion/proto/mctrl_nav.py +0 -664
  128. pymammotion/proto/mctrl_ota.py +0 -48
  129. pymammotion/proto/mctrl_pept.py +0 -41
  130. pymammotion/proto/mctrl_sys.py +0 -574
  131. pymammotion-0.4.0a2.dist-info/RECORD +0 -131
  132. /pymammotion/http/{_init_.py → __init__.py} +0 -0
  133. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info/licenses}/LICENSE +0 -0
@@ -1,5 +1,4 @@
1
1
  from dataclasses import dataclass
2
- from typing import Optional
3
2
 
4
3
  from mashumaro.config import BaseConfig
5
4
  from mashumaro.mixins.orjson import DataClassORJSONMixin
@@ -16,7 +15,7 @@ class DeviceData(DataClassORJSONMixin):
16
15
  class AepResponse(DataClassORJSONMixin):
17
16
  code: int
18
17
  data: DeviceData
19
- id: Optional[str] = None
18
+ id: str | None = None
20
19
 
21
20
  class Config(BaseConfig):
22
21
  omit_default = True
@@ -1,36 +1,183 @@
1
1
  from dataclasses import dataclass
2
- from typing import Optional
2
+ from typing import Annotated, Optional
3
3
 
4
4
  from mashumaro.config import BaseConfig
5
5
  from mashumaro.mixins.orjson import DataClassORJSONMixin
6
+ from mashumaro.types import Alias
6
7
 
7
8
 
8
9
  @dataclass
9
10
  class Device(DataClassORJSONMixin):
10
- gmtModified: int
11
- netType: str
12
- categoryKey: str
13
- productKey: str
14
- nodeType: str
15
- isEdgeGateway: bool
16
- deviceName: str
17
- categoryName: str
18
- identityAlias: str
19
- productName: str
20
- iotId: str
21
- bindTime: int
22
- owned: int
23
- identityId: str
24
- thingType: str
11
+ """Unified device model supporting both Device and ShareNotification data"""
12
+
13
+ # Core device fields (from Device model)
14
+ gmt_modified: Annotated[int, Alias("gmtModified")]
15
+ node_type: Annotated[str, Alias("nodeType")]
16
+ device_name: Annotated[str, Alias("deviceName")]
17
+ product_name: Annotated[str, Alias("productName")]
25
18
  status: int
26
- nickName: Optional[str] = None
19
+ identity_id: Annotated[str, Alias("identityId")]
20
+
21
+ # Required fields from original Device model
22
+ net_type: Annotated[str, Alias("netType")]
23
+ category_key: Annotated[str, Alias("categoryKey")]
24
+ product_key: Annotated[str, Alias("productKey")]
25
+ is_edge_gateway: Annotated[bool, Alias("isEdgeGateway")]
26
+ category_name: Annotated[str, Alias("categoryName")]
27
+ identity_alias: Annotated[str, Alias("identityAlias")]
28
+ iot_id: Annotated[str, Alias("iotId")]
29
+ bind_time: Annotated[int, Alias("bindTime")]
30
+ owned: int
31
+ thing_type: Annotated[str, Alias("thingType")]
32
+
33
+ # Optional fields (common to both or nullable)
34
+ nick_name: Annotated[Optional[str], Alias("nickName")] = None
27
35
  description: Optional[str] = None
28
- productImage: Optional[str] = None
29
- categoryImage: Optional[str] = None
30
- productModel: Optional[str] = None
36
+ product_image: Annotated[Optional[str], Alias("productImage")] = None
37
+ category_image: Annotated[Optional[str], Alias("categoryImage")] = None
38
+ product_model: Annotated[Optional[str], Alias("productModel")] = None
39
+
40
+ # Optional fields from ShareNotification only
41
+ target_id: Annotated[Optional[str], Alias("targetId")] = None
42
+ receiver_identity_id: Annotated[Optional[str], Alias("receiverIdentityId")] = None
43
+ target_type: Annotated[Optional[str], Alias("targetType")] = None
44
+ gmt_create: Annotated[Optional[int], Alias("gmtCreate")] = None
45
+ batch_id: Annotated[Optional[str], Alias("batchId")] = None
46
+ record_id: Annotated[Optional[str], Alias("recordId")] = None
47
+ initiator_identity_id: Annotated[Optional[str], Alias("initiatorIdentityId")] = None
48
+ is_receiver: Annotated[Optional[int], Alias("isReceiver")] = None
49
+ initiator_alias: Annotated[Optional[str], Alias("initiatorAlias")] = None
50
+ receiver_alias: Annotated[Optional[str], Alias("receiverAlias")] = None
31
51
 
32
52
  class Config(BaseConfig):
33
53
  omit_default = True
54
+ allow_deserialization_not_by_alias = True
55
+
56
+
57
+ # # Alternative: Keep them separate but with a common base class
58
+ # @dataclass
59
+ # class BaseDevice(DataClassORJSONMixin):
60
+ # """Base device model with common fields"""
61
+ #
62
+ # gmt_modified: int
63
+ # node_type: str
64
+ # device_name: str
65
+ # product_name: str
66
+ # status: int
67
+ # product_image: Optional[str] = None
68
+ # category_image: Optional[str] = None
69
+ # description: Optional[str] = None
70
+ #
71
+ # class Config(BaseConfig):
72
+ # omit_default = True
73
+ # serialize_by_alias = True
74
+ # aliases = {
75
+ # "gmt_modified": "gmtModified",
76
+ # "node_type": "nodeType",
77
+ # "device_name": "deviceName",
78
+ # "product_name": "productName",
79
+ # "product_image": "productImage",
80
+ # "category_image": "categoryImage",
81
+ # }
82
+ #
83
+ #
84
+ # @dataclass
85
+ # class Device(BaseDevice):
86
+ # """Full device model"""
87
+ #
88
+ # net_type: str
89
+ # category_key: str
90
+ # product_key: str
91
+ # is_edge_gateway: bool
92
+ # category_name: str
93
+ # identity_alias: str
94
+ # iot_id: str
95
+ # bind_time: int
96
+ # owned: int
97
+ # identity_id: str
98
+ # thing_type: str
99
+ # nick_name: Optional[str] = None
100
+ # product_model: Optional[str] = None
101
+ #
102
+ # class Config(BaseConfig):
103
+ # omit_default = True
104
+ # serialize_by_alias = True
105
+ # aliases = {
106
+ # **BaseDevice.Config.aliases,
107
+ # "net_type": "netType",
108
+ # "category_key": "categoryKey",
109
+ # "product_key": "productKey",
110
+ # "is_edge_gateway": "isEdgeGateway",
111
+ # "category_name": "categoryName",
112
+ # "identity_alias": "identityAlias",
113
+ # "iot_id": "iotId",
114
+ # "bind_time": "bindTime",
115
+ # "identity_id": "identityId",
116
+ # "thing_type": "thingType",
117
+ # "nick_name": "nickName",
118
+ # "product_model": "productModel",
119
+ # }
120
+ #
121
+ #
122
+ # @dataclass
123
+ # class ShareNotification(BaseDevice):
124
+ # """Share notification model extending base device"""
125
+ #
126
+ # target_id: str
127
+ # receiver_identity_id: str
128
+ # target_type: str
129
+ # gmt_create: int
130
+ # batch_id: str
131
+ # record_id: str
132
+ # initiator_identity_id: str
133
+ # is_receiver: int
134
+ # initiator_alias: str
135
+ # receiver_alias: str
136
+ #
137
+ # # Optional fields that Device has but ShareNotification might not
138
+ # net_type: Optional[str] = None
139
+ # category_key: Optional[str] = None
140
+ # product_key: Optional[str] = None
141
+ # is_edge_gateway: Optional[bool] = None
142
+ # category_name: Optional[str] = None
143
+ # identity_alias: Optional[str] = None
144
+ # iot_id: Optional[str] = None
145
+ # bind_time: Optional[int] = None
146
+ # owned: Optional[int] = None
147
+ # identity_id: Optional[str] = None
148
+ # thing_type: Optional[str] = None
149
+ # nick_name: Optional[str] = None
150
+ # product_model: Optional[str] = None
151
+ #
152
+ # class Config(BaseConfig):
153
+ # omit_default = True
154
+ # serialize_by_alias = True
155
+ # aliases = {
156
+ # **BaseDevice.Config.aliases,
157
+ # "target_id": "targetId",
158
+ # "receiver_identity_id": "receiverIdentityId",
159
+ # "target_type": "targetType",
160
+ # "gmt_create": "gmtCreate",
161
+ # "batch_id": "batchId",
162
+ # "record_id": "recordId",
163
+ # "initiator_identity_id": "initiatorIdentityId",
164
+ # "is_receiver": "isReceiver",
165
+ # "initiator_alias": "initiatorAlias",
166
+ # "receiver_alias": "receiverAlias",
167
+ # # Device fields that might be present
168
+ # "net_type": "netType",
169
+ # "category_key": "categoryKey",
170
+ # "product_key": "productKey",
171
+ # "is_edge_gateway": "isEdgeGateway",
172
+ # "category_name": "categoryName",
173
+ # "identity_alias": "identityAlias",
174
+ # "iot_id": "iotId",
175
+ # "bind_time": "bindTime",
176
+ # "identity_id": "identityId",
177
+ # "thing_type": "thingType",
178
+ # "nick_name": "nickName",
179
+ # "product_model": "productModel",
180
+ # }
34
181
 
35
182
 
36
183
  @dataclass
@@ -42,7 +189,7 @@ class Data(DataClassORJSONMixin):
42
189
 
43
190
 
44
191
  @dataclass
45
- class ListingDevByAccountResponse(DataClassORJSONMixin):
192
+ class ListingDevAccountResponse(DataClassORJSONMixin):
46
193
  code: int
47
- data: Optional[Data]
48
- id: Optional[str] = None
194
+ data: Data | None
195
+ id: str | None = None
@@ -1,5 +1,4 @@
1
1
  from dataclasses import dataclass
2
- from typing import Optional
3
2
 
4
3
  from mashumaro.mixins.orjson import DataClassORJSONMixin
5
4
 
@@ -18,7 +17,7 @@ class OpenAccount(DataClassORJSONMixin):
18
17
  domainId: int
19
18
  enableDevice: str
20
19
  status: int
21
- country: Optional[str] = None
20
+ country: str | None = None
22
21
 
23
22
 
24
23
  @dataclass
@@ -54,7 +53,7 @@ class InnerData(DataClassORJSONMixin):
54
53
  subCode: int
55
54
  message: str
56
55
  successful: str
57
- deviceId: Optional[str] = None
56
+ deviceId: str | None = None
58
57
 
59
58
 
60
59
  @dataclass
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass
2
- from typing import Optional, TypeVar
2
+ from typing import TypeVar
3
3
 
4
4
  from mashumaro.config import BaseConfig
5
5
  from mashumaro.mixins.orjson import DataClassORJSONMixin
@@ -22,8 +22,8 @@ class RegionResponseData(DataClassORJSONMixin):
22
22
  class RegionResponse(DataClassORJSONMixin):
23
23
  data: RegionResponseData
24
24
  code: int
25
- id: Optional[str] = None
26
- msg: Optional[str] = None
25
+ id: str | None = None
26
+ msg: str | None = None
27
27
 
28
28
  class Config(BaseConfig):
29
29
  omit_default = True
@@ -1,5 +1,4 @@
1
1
  from dataclasses import dataclass
2
- from typing import Optional
3
2
 
4
3
  from mashumaro.mixins.orjson import DataClassORJSONMixin
5
4
 
@@ -16,4 +15,5 @@ class SessionOauthToken(DataClassORJSONMixin):
16
15
  @dataclass
17
16
  class SessionByAuthCodeResponse(DataClassORJSONMixin):
18
17
  code: int
19
- data: Optional[SessionOauthToken] = None
18
+ data: SessionOauthToken | None = None
19
+ token_issued_at: int | None = None
@@ -0,0 +1,12 @@
1
+ from dataclasses import dataclass
2
+
3
+ from mashumaro.mixins.orjson import DataClassORJSONMixin
4
+
5
+ from pymammotion.data.mqtt.properties import Items
6
+
7
+
8
+ @dataclass
9
+ class ThingPropertiesResponse(DataClassORJSONMixin):
10
+ code: int
11
+ data: Items | None
12
+ id: str | None = None
@@ -0,0 +1,62 @@
1
+ region_mappings = {
2
+ # Asia Pacific
3
+ "CN": "cn-hongkong", # China -> Hong Kong
4
+ "HK": "cn-hongkong", # Hong Kong
5
+ "TW": "cn-hongkong", # Taiwan -> Hong Kong
6
+ "MO": "cn-hongkong", # Macau -> Hong Kong
7
+ # Southeast Asia
8
+ "SG": "ap-southeast-1", # Singapore
9
+ "MY": "ap-southeast-3", # Malaysia
10
+ "ID": "ap-southeast-5", # Indonesia
11
+ "TH": "ap-southeast-1", # Thailand -> Singapore
12
+ "VN": "ap-southeast-1", # Vietnam -> Singapore
13
+ "PH": "ap-southeast-1", # Philippines -> Singapore
14
+ "BN": "ap-southeast-1", # Brunei -> Singapore
15
+ "KH": "ap-southeast-1", # Cambodia -> Singapore
16
+ "LA": "ap-southeast-1", # Laos -> Singapore
17
+ "MM": "ap-southeast-1", # Myanmar -> Singapore
18
+ # Australia/Oceania
19
+ "AU": "ap-southeast-2", # Australia
20
+ "NZ": "ap-southeast-2", # New Zealand -> Australia
21
+ "FJ": "ap-southeast-2", # Fiji -> Australia
22
+ "PG": "ap-southeast-2", # Papua New Guinea -> Australia
23
+ # South Asia
24
+ "IN": "ap-south-1", # India
25
+ "LK": "ap-south-1", # Sri Lanka -> India
26
+ "PK": "ap-south-1", # Pakistan -> India
27
+ "BD": "ap-south-1", # Bangladesh -> India
28
+ "NP": "ap-south-1", # Nepal -> India
29
+ "BT": "ap-south-1", # Bhutan -> India
30
+ "MV": "ap-south-1", # Maldives -> India
31
+ # Middle East
32
+ "AE": "me-east-1", # UAE
33
+ "SA": "me-east-1", # Saudi Arabia -> UAE
34
+ "QA": "me-east-1", # Qatar -> UAE
35
+ "KW": "me-east-1", # Kuwait -> UAE
36
+ "BH": "me-east-1", # Bahrain -> UAE
37
+ "OM": "me-east-1", # Oman -> UAE
38
+ "IL": "me-east-1", # Israel -> UAE
39
+ "TR": "me-east-1", # Turkey -> UAE
40
+ # Europe
41
+ "DE": "eu-central-1", # Germany
42
+ "FR": "eu-central-1", # France -> Germany
43
+ "IT": "eu-central-1", # Italy -> Germany
44
+ "ES": "eu-central-1", # Spain -> Germany
45
+ "GB": "eu-central-1", # UK -> Germany
46
+ "IE": "eu-central-1", # Ireland -> Germany
47
+ "NL": "eu-central-1", # Netherlands -> Germany
48
+ "BE": "eu-central-1", # Belgium -> Germany
49
+ "CH": "eu-central-1", # Switzerland -> Germany
50
+ "AT": "eu-central-1", # Austria -> Germany
51
+ "PL": "eu-central-1", # poland -> Germany
52
+ # North America
53
+ "US": "us-east-1", # USA
54
+ "CA": "us-east-1", # Canada -> US East
55
+ "MX": "us-west-1", # Mexico -> US West
56
+ # South America
57
+ "BR": "us-east-1", # Brazil -> US East
58
+ "AR": "us-east-1", # Argentina -> US East
59
+ "CL": "us-east-1", # Chile -> US East
60
+ "CO": "us-east-1", # Colombia -> US East
61
+ "PE": "us-east-1", # Peru -> US East
62
+ }
@@ -0,0 +1,297 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import socket
5
+ import ssl
6
+ import time
7
+ from typing import Any
8
+ from urllib.parse import urlencode, urlparse
9
+
10
+ import aiohttp
11
+ import certifi
12
+ from requests import PreparedRequest, adapters, status_codes
13
+ from Tea.exceptions import RequiredArgumentException, RetryError
14
+ from Tea.model import TeaModel
15
+ from Tea.request import TeaRequest
16
+ from Tea.response import TeaResponse
17
+ from Tea.stream import BaseStream
18
+
19
+ DEFAULT_CONNECT_TIMEOUT = 5000
20
+ DEFAULT_READ_TIMEOUT = 10000
21
+ DEFAULT_POOL_SIZE = 10
22
+
23
+ logger = logging.getLogger("alibabacloud-tea")
24
+ logger.setLevel(logging.DEBUG)
25
+ ch = logging.StreamHandler()
26
+ logger.addHandler(ch)
27
+
28
+
29
+ class TeaCore:
30
+ http_adapter = adapters.HTTPAdapter(pool_connections=DEFAULT_POOL_SIZE, pool_maxsize=DEFAULT_POOL_SIZE * 4)
31
+ https_adapter = adapters.HTTPAdapter(pool_connections=DEFAULT_POOL_SIZE, pool_maxsize=DEFAULT_POOL_SIZE * 4)
32
+
33
+ @staticmethod
34
+ def get_adapter(prefix):
35
+ if prefix.upper() == "HTTP":
36
+ return TeaCore.http_adapter
37
+ else:
38
+ return TeaCore.https_adapter
39
+
40
+ @staticmethod
41
+ def _prepare_http_debug(request, symbol):
42
+ base = ""
43
+ for key, value in request.headers.items():
44
+ base += f"\n{symbol} {key} : {value}"
45
+ return base
46
+
47
+ @staticmethod
48
+ def _do_http_debug(request, response) -> None:
49
+ # logger the request
50
+ url = urlparse(request.url)
51
+ request_base = f"\n> {request.method.upper()} {url.path + url.query} HTTP/1.1"
52
+ logger.debug(request_base + TeaCore._prepare_http_debug(request, ">"))
53
+
54
+ # logger the response
55
+ response_base = (
56
+ f"\n< HTTP/1.1 {response.status_code}" f" {status_codes._codes.get(response.status_code)[0].upper()}"
57
+ )
58
+ logger.debug(response_base + TeaCore._prepare_http_debug(response, "<"))
59
+
60
+ @staticmethod
61
+ def compose_url(request):
62
+ host = request.headers.get("host")
63
+ if not host:
64
+ raise RequiredArgumentException("endpoint")
65
+ else:
66
+ host = host.rstrip("/")
67
+ protocol = f"{request.protocol.lower()}://"
68
+ pathname = request.pathname
69
+
70
+ if host.startswith(("http://", "https://")):
71
+ protocol = ""
72
+
73
+ if request.port == 80:
74
+ port = ""
75
+ else:
76
+ port = f":{request.port}"
77
+
78
+ url = protocol + host + port + pathname
79
+
80
+ if request.query:
81
+ if "?" in url:
82
+ if not url.endswith("&"):
83
+ url += "&"
84
+ else:
85
+ url += "?"
86
+
87
+ encode_query = {}
88
+ for key in request.query:
89
+ value = request.query[key]
90
+ if value is not None:
91
+ encode_query[key] = str(value)
92
+ url += urlencode(encode_query)
93
+ return url.rstrip("?&")
94
+
95
+ @staticmethod
96
+ async def async_do_action(request: TeaRequest, runtime_option=None) -> TeaResponse:
97
+ runtime_option = runtime_option or {}
98
+
99
+ url = TeaCore.compose_url(request)
100
+ verify = not runtime_option.get("ignoreSSL", False)
101
+
102
+ timeout = runtime_option.get("timeout")
103
+ connect_timeout = runtime_option.get("connectTimeout") or timeout or DEFAULT_CONNECT_TIMEOUT
104
+ read_timeout = runtime_option.get("readTimeout") or timeout or DEFAULT_READ_TIMEOUT
105
+
106
+ connect_timeout, read_timeout = (int(connect_timeout) / 1000, int(read_timeout) / 1000)
107
+
108
+ proxy = None
109
+ if request.protocol.upper() == "HTTP":
110
+ proxy = runtime_option.get("httpProxy")
111
+ if not proxy:
112
+ proxy = os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
113
+ elif request.protocol.upper() == "HTTPS":
114
+ proxy = runtime_option.get("httpsProxy")
115
+ if not proxy:
116
+ proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
117
+
118
+ connector = None
119
+ ca_cert = certifi.where()
120
+ if ca_cert and request.protocol.upper() == "HTTPS":
121
+ loop = asyncio.get_event_loop()
122
+
123
+ ssl_context = await loop.run_in_executor(None, ssl.create_default_context, ssl.Purpose.SERVER_AUTH)
124
+ await loop.run_in_executor(None, ssl_context.load_verify_locations, ca_cert)
125
+ connector = aiohttp.TCPConnector(
126
+ ssl=ssl_context,
127
+ family=socket.AF_INET,
128
+ )
129
+ else:
130
+ verify = False
131
+
132
+ timeout = aiohttp.ClientTimeout(sock_read=read_timeout, sock_connect=connect_timeout)
133
+ async with aiohttp.ClientSession(connector=connector) as s:
134
+ body = b""
135
+ if isinstance(request.body, BaseStream):
136
+ for content in request.body:
137
+ body += content
138
+ elif isinstance(request.body, str):
139
+ body = request.body.encode("utf-8")
140
+ else:
141
+ body = request.body
142
+ try:
143
+ async with s.request(
144
+ request.method, url, data=body, headers=request.headers, ssl=verify, proxy=proxy, timeout=timeout
145
+ ) as response:
146
+ tea_resp = TeaResponse()
147
+ tea_resp.body = await response.read()
148
+ tea_resp.headers = {k.lower(): v for k, v in response.headers.items()}
149
+ tea_resp.status_code = response.status
150
+ tea_resp.status_message = response.reason
151
+ tea_resp.response = response
152
+ except OSError as e:
153
+ raise RetryError(str(e))
154
+ return tea_resp
155
+
156
+ @staticmethod
157
+ def do_action(request: TeaRequest, runtime_option=None) -> TeaResponse:
158
+ url = TeaCore.compose_url(request)
159
+
160
+ runtime_option = runtime_option or {}
161
+
162
+ verify = not runtime_option.get("ignoreSSL", False)
163
+ if verify:
164
+ verify = runtime_option.get("ca", True) if runtime_option.get("ca", True) is not None else True
165
+ cert = runtime_option.get("cert", None)
166
+
167
+ timeout = runtime_option.get("timeout")
168
+ connect_timeout = runtime_option.get("connectTimeout") or timeout or DEFAULT_CONNECT_TIMEOUT
169
+ read_timeout = runtime_option.get("readTimeout") or timeout or DEFAULT_READ_TIMEOUT
170
+
171
+ timeout = (int(connect_timeout) / 1000, int(read_timeout) / 1000)
172
+
173
+ if isinstance(request.body, str):
174
+ request.body = request.body.encode("utf-8")
175
+
176
+ p = PreparedRequest()
177
+ p.prepare(
178
+ method=request.method.upper(),
179
+ url=url,
180
+ data=request.body,
181
+ headers=request.headers,
182
+ )
183
+
184
+ proxies = {}
185
+ http_proxy = runtime_option.get("httpProxy")
186
+ https_proxy = runtime_option.get("httpsProxy")
187
+ no_proxy = runtime_option.get("noProxy")
188
+
189
+ if not http_proxy:
190
+ http_proxy = os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
191
+ if not https_proxy:
192
+ https_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
193
+
194
+ if http_proxy:
195
+ proxies["http"] = http_proxy
196
+ if https_proxy:
197
+ proxies["https"] = https_proxy
198
+ if no_proxy:
199
+ proxies["no_proxy"] = no_proxy
200
+
201
+ adapter = TeaCore.get_adapter(request.protocol)
202
+ try:
203
+ resp = adapter.send(
204
+ p,
205
+ proxies=proxies,
206
+ timeout=timeout,
207
+ verify=verify,
208
+ cert=cert,
209
+ )
210
+ except OSError as e:
211
+ raise RetryError(str(e))
212
+
213
+ debug = runtime_option.get("debug") or os.getenv("DEBUG")
214
+ if debug and debug.lower() == "sdk":
215
+ TeaCore._do_http_debug(p, resp)
216
+
217
+ response = TeaResponse()
218
+ response.status_message = resp.reason
219
+ response.status_code = resp.status_code
220
+ response.headers = {k.lower(): v for k, v in resp.headers.items()}
221
+ response.body = resp.content
222
+ response.response = resp
223
+ return response
224
+
225
+ @staticmethod
226
+ def get_response_body(resp) -> str:
227
+ return resp.content.decode("utf-8")
228
+
229
+ @staticmethod
230
+ def allow_retry(dic, retry_times, now=None) -> bool:
231
+ if retry_times == 0:
232
+ return True
233
+ if dic is None or not dic.__contains__("maxAttempts") or dic.get("retryable") is not True and retry_times >= 1:
234
+ return False
235
+ else:
236
+ retry = 0 if dic.get("maxAttempts") is None else int(dic.get("maxAttempts"))
237
+ return retry >= retry_times
238
+
239
+ @staticmethod
240
+ def get_backoff_time(dic, retry_times) -> int:
241
+ default_back_off_time = 0
242
+ if dic is None or not dic.get("policy") or dic.get("policy") == "no":
243
+ return default_back_off_time
244
+
245
+ back_off_time = dic.get("period", default_back_off_time)
246
+ if not isinstance(back_off_time, int) and not (isinstance(back_off_time, str) and back_off_time.isdigit()):
247
+ return default_back_off_time
248
+
249
+ back_off_time = int(back_off_time)
250
+ if back_off_time < 0:
251
+ return retry_times
252
+
253
+ return back_off_time
254
+
255
+ @staticmethod
256
+ async def sleep_async(t) -> None:
257
+ await asyncio.sleep(t)
258
+
259
+ @staticmethod
260
+ def sleep(t) -> None:
261
+ time.sleep(t)
262
+
263
+ @staticmethod
264
+ def is_retryable(ex) -> bool:
265
+ return isinstance(ex, RetryError)
266
+
267
+ @staticmethod
268
+ def bytes_readable(body):
269
+ return body
270
+
271
+ @staticmethod
272
+ def merge(*dic_list) -> dict:
273
+ dic_result = {}
274
+ for item in dic_list:
275
+ if isinstance(item, dict):
276
+ dic_result.update(item)
277
+ elif isinstance(item, TeaModel):
278
+ dic_result.update(item.to_map())
279
+ return dic_result
280
+
281
+ @staticmethod
282
+ def to_map(model: TeaModel | None) -> dict[str, Any]:
283
+ if isinstance(model, TeaModel):
284
+ return model.to_map()
285
+ else:
286
+ return dict()
287
+
288
+ @staticmethod
289
+ def from_map(model: TeaModel, dic: dict[str, Any]) -> TeaModel:
290
+ if isinstance(model, TeaModel):
291
+ try:
292
+ return model.from_map(dic)
293
+ except Exception:
294
+ model._map = dic
295
+ return model
296
+ else:
297
+ return model