cloudoll 2.4.0__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.
cloudoll/web/http.py ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ __author__ = "chuchur/chuchur.com"
5
+ # from aiohttp.web import middleware
6
+ from curl_cffi import requests
7
+
8
+
9
+ class Client(object):
10
+ def __init__(self):
11
+ self.session = requests.Session()
12
+
13
+ def requests(self, method, url, **kw) -> requests.Response:
14
+ impersonate = kw.get("impersonate", None)
15
+ if not impersonate:
16
+ kw["impersonate"] = "chrome133a"
17
+ fun = getattr(self.session, method, None)
18
+ result: requests.Response = fun(url, **kw)
19
+ return result
20
+
21
+
22
+ http = Client()
23
+
24
+
25
+ def get(url, **kw):
26
+ """
27
+ get 请求
28
+ :params params=dict() 传参
29
+ """
30
+ return http.requests("get", url, **kw)
31
+
32
+
33
+ def post(url, **kw):
34
+ """
35
+ post 请求
36
+ :params data=dict() or json=dict() 传参
37
+ """
38
+ return http.requests("post", url, **kw)
39
+
40
+
41
+ def put(url, **kw):
42
+ """
43
+ post 请求
44
+ data=dict()传参
45
+ """
46
+ return http.requests("put", url, **kw)
47
+
48
+
49
+ def delete(url, **kw):
50
+ return http.requests("delete", url, **kw)
51
+
52
+
53
+ def head(url, **kw):
54
+ return http.requests("head", url, **kw)
55
+
56
+
57
+ def option(url, **kw):
58
+ return http.requests("option", url, **kw)
59
+
60
+
61
+ def download(url, savepath=None, **kw):
62
+ """
63
+ 下载文件
64
+ :params url 文件的地址
65
+ :params savepath 保存路径
66
+ """
67
+ response = http.requests("get", url, **kw)
68
+ if response.status_code == 200:
69
+ # 如果传入了保存路径,保存文件
70
+ if savepath:
71
+ with open(savepath, "wb") as file:
72
+ file.write(response.content)
73
+ print(f"File saved to {savepath}")
74
+ else:
75
+ # 如果没有传入保存路径,返回文件内容
76
+ return response.content
77
+ else:
78
+ print(f"Failed to download the file. Status code: {response.status_code}")
79
+ return None
cloudoll/web/jwt.py ADDED
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ __author__ = "chuchur/chuchur.com"
5
+
6
+ import jwt, datetime
7
+ from ..logging import print_error
8
+ from typing import Union
9
+
10
+ def encode(payload, key, exp: Union[int, str] = 3600)->str:
11
+ """
12
+ jwt 加密
13
+ :params payload
14
+ :params key
15
+ :params exp 过期时间单位秒,默认1个小时
16
+ """
17
+ headers = dict(typ="jwt", alg="HS256")
18
+ exp = datetime.datetime.now() + datetime.timedelta(seconds=eval(exp) if isinstance(exp, str) else exp) # 过期时间
19
+ payload["exp"] = exp.timestamp()
20
+ result = jwt.encode(payload=payload, key=key, algorithm="HS256", headers=headers)
21
+ return result
22
+
23
+
24
+ def decode(token, key):
25
+ """
26
+ jwt
27
+ :params token
28
+ :params key
29
+ """
30
+ try:
31
+ payload = jwt.decode(token, key, algorithms=['HS256'])
32
+ if not payload:
33
+ return None
34
+ now = datetime.datetime.now().timestamp() # 当前时间
35
+ if int(now) > int(payload["exp"]): # 登录时间过期
36
+ return None
37
+ return payload # 返回自定义内容
38
+ except Exception as e:
39
+ print_error(e)
40
+ return None
@@ -0,0 +1,161 @@
1
+ import aiohttp
2
+ import asyncio
3
+ from typing import Optional, Dict, Any, Union
4
+
5
+
6
+ class AsyncRequest:
7
+ def __init__(
8
+ self,
9
+ timeout: int = 30,
10
+ max_retries: int = 3,
11
+ retry_delay: float = 1.0,
12
+ session: Optional[aiohttp.ClientSession] = None,
13
+ ):
14
+ """
15
+ 异步HTTP请求客户端
16
+
17
+ :param timeout: 请求超时时间(秒)
18
+ :param max_retries: 最大重试次数
19
+ :param retry_delay: 重试延迟(秒)
20
+ :param session: 可复用的aiohttp会话
21
+ """
22
+ self.timeout = aiohttp.ClientTimeout(total=timeout)
23
+ self.max_retries = max_retries
24
+ self.retry_delay = retry_delay
25
+ self._session = session
26
+
27
+ async def request(
28
+ self,
29
+ method: str,
30
+ url: str,
31
+ params: Optional[Dict[str, Any]] = None,
32
+ data: Optional[Union[Dict[str, Any], str, bytes]] = None,
33
+ json_data: Optional[Any] = None,
34
+ headers: Optional[Dict[str, str]] = None,
35
+ cookies: Optional[Dict[str, str]] = None,
36
+ allow_redirects: bool = True,
37
+ raise_for_status: bool = True,
38
+ ) -> aiohttp.ClientResponse:
39
+ """
40
+ 执行异步HTTP请求
41
+
42
+ :param method: HTTP方法 (GET, POST, PUT, DELETE等)
43
+ :param url: 请求URL
44
+ :param params: URL查询参数
45
+ :param data: 请求体数据 (表单或原始数据)
46
+ :param json_data: JSON格式的请求体数据
47
+ :param headers: 请求头
48
+ :param cookies: cookies
49
+ :param allow_redirects: 是否允许重定向
50
+ :param raise_for_status: 是否在HTTP错误状态时抛出异常
51
+ :return: aiohttp.ClientResponse
52
+ """
53
+ session = self._session or aiohttp.ClientSession(timeout=self.timeout)
54
+ last_exception = None
55
+
56
+ for attempt in range(self.max_retries):
57
+ try:
58
+ async with session.request(
59
+ method=method,
60
+ url=url,
61
+ params=params,
62
+ data=data,
63
+ json=json_data,
64
+ headers=headers,
65
+ cookies=cookies,
66
+ allow_redirects=allow_redirects,
67
+ ) as response:
68
+ if raise_for_status:
69
+ response.raise_for_status()
70
+ return response
71
+
72
+ except (aiohttp.ClientError, asyncio.TimeoutError) as e:
73
+ last_exception = e
74
+ if attempt < self.max_retries - 1:
75
+ await asyncio.sleep(self.retry_delay * (attempt + 1))
76
+ continue
77
+
78
+ if self._session is None:
79
+ await session.close()
80
+
81
+ raise last_exception or RuntimeError("Request failed")
82
+
83
+ async def get(
84
+ self,
85
+ url: str,
86
+ params: Optional[Dict[str, Any]] = None,
87
+ headers: Optional[Dict[str, str]] = None,
88
+ **kwargs
89
+ ) -> aiohttp.ClientResponse:
90
+ """发送GET请求"""
91
+ return await self.request("GET", url, params=params, headers=headers, **kwargs)
92
+
93
+ async def post(
94
+ self,
95
+ url: str,
96
+ data: Optional[Union[Dict[str, Any], str, bytes]] = None,
97
+ json_data: Optional[Any] = None,
98
+ headers: Optional[Dict[str, str]] = None,
99
+ **kwargs
100
+ ) -> aiohttp.ClientResponse:
101
+ """发送POST请求"""
102
+ return await self.request(
103
+ "POST", url, data=data, json_data=json_data, headers=headers, **kwargs
104
+ )
105
+
106
+ async def put(
107
+ self,
108
+ url: str,
109
+ data: Optional[Union[Dict[str, Any], str, bytes]] = None,
110
+ json_data: Optional[Any] = None,
111
+ headers: Optional[Dict[str, str]] = None,
112
+ **kwargs
113
+ ) -> aiohttp.ClientResponse:
114
+ """发送PUT请求"""
115
+ return await self.request(
116
+ "PUT", url, data=data, json_data=json_data, headers=headers, **kwargs
117
+ )
118
+
119
+ async def delete(
120
+ self, url: str, headers: Optional[Dict[str, str]] = None, **kwargs
121
+ ) -> aiohttp.ClientResponse:
122
+ """发送DELETE请求"""
123
+ return await self.request("DELETE", url, headers=headers, **kwargs)
124
+
125
+ async def close(self) -> None:
126
+ """关闭客户端会话"""
127
+ if self._session is not None:
128
+ await self._session.close()
129
+ self._session = None
130
+
131
+ async def __aenter__(self):
132
+ return self
133
+
134
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
135
+ await self.close()
136
+
137
+
138
+ # 使用示例
139
+ """
140
+ async def test():
141
+ async with AsyncRequest() as client:
142
+ try:
143
+ # GET请求示例
144
+ response = await client.get(
145
+ "https://httpbin.org/get",
146
+ params={"key": "value"},
147
+ headers={"User-Agent": "AsyncRequest/1.0"},
148
+ )
149
+ print(await response.json())
150
+
151
+ # POST请求示例
152
+ response = await client.post(
153
+ "https://httpbin.org/post",
154
+ json_data={"key": "value"},
155
+ headers={"Content-Type": "application/json"},
156
+ )
157
+ print(await response.json())
158
+
159
+ except Exception as e:
160
+ print(f"Request failed: {e}")
161
+ """
@@ -0,0 +1,22 @@
1
+ import yaml
2
+ import os
3
+ from ..logging import error, info
4
+ from envyaml import EnvYAML
5
+
6
+
7
+ def get_config(env):
8
+ if env is None:
9
+ return {}
10
+ conf_path = os.path.join(os.path.abspath("."), "config", f"conf.{env}.yaml")
11
+ info(f"loading config {conf_path}")
12
+ if not os.path.exists(conf_path):
13
+ error(f"Configuration file does not exist: {conf_path}")
14
+ return {}
15
+ with open(conf_path, "r", encoding="utf-8") as f:
16
+ config = yaml.safe_load(f)
17
+ try:
18
+ config_ori = EnvYAML(conf_path, strict=False)
19
+ config = dict(config_ori)
20
+ except BaseException as e:
21
+ error(e)
22
+ return config or {}
@@ -0,0 +1,414 @@
1
+ Metadata-Version: 2.4
2
+ Name: cloudoll
3
+ Version: 2.4.0
4
+ Summary: 辅助快速创建可分布的微服务。
5
+ Home-page: https://gitee.com/chuchur/cloudoll-py
6
+ Author: chuchur
7
+ Author-email: chuchur@qq.com
8
+ License: MIT
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.6
13
+ Classifier: Programming Language :: Python :: Implementation :: CPython
14
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
15
+ Requires-Python: >=3.6.0
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: watchfiles
19
+ Requires-Dist: colorlog
20
+ Requires-Dist: aiomysql
21
+ Requires-Dist: aiopg
22
+ Requires-Dist: aiohttp
23
+ Requires-Dist: redis[hiredis]
24
+ Requires-Dist: aiomcache
25
+ Requires-Dist: jinja2
26
+ Requires-Dist: pyjwt
27
+ Requires-Dist: aiohttp_session[secure]
28
+ Requires-Dist: PyYAML
29
+ Requires-Dist: click
30
+ Requires-Dist: envyaml
31
+ Dynamic: author
32
+ Dynamic: author-email
33
+ Dynamic: classifier
34
+ Dynamic: description
35
+ Dynamic: description-content-type
36
+ Dynamic: home-page
37
+ Dynamic: license
38
+ Dynamic: license-file
39
+ Dynamic: requires-dist
40
+ Dynamic: requires-python
41
+ Dynamic: summary
42
+
43
+
44
+ # cloudoll 云端玩具
45
+
46
+ ## 更新日志
47
+
48
+ `2.2.28` 2023-07-04
49
+ - 多数据库支持
50
+ - 优化orm
51
+ -
52
+ `2.0.14` 2023-07-04
53
+ - 加入cli,devtoll
54
+ - 优化orm
55
+ -
56
+ `2.0.13` 2023-07-04
57
+ - 修复已知 问题
58
+ -
59
+ `2.0.12` 2023-07-04
60
+ - 修复已知 问题
61
+ -
62
+ `2.0.11` 2023-07-04
63
+ - 修复已知 问题
64
+ -
65
+ `2.0.10` 2023-07-04
66
+ - 修复已知 问题
67
+ -
68
+ `2.0.9` 2023-07-04
69
+ - 修复已知 问题
70
+ -
71
+ `2.0.8` 2023-07-04
72
+ - 修复已知 问题
73
+ -
74
+ `2.0.7` 2023-07-04
75
+ - 修复logging 问题
76
+
77
+ `2.0.5` 2023-07-04
78
+ - 修复redis在 3.11+环境下的问题
79
+
80
+ `2.0.4` 2023-07-04
81
+ - 切换热更为adev
82
+
83
+
84
+ `2.0.2` 2023-07-03
85
+ - 优化一系列问题
86
+ - 可以热加载
87
+
88
+ `2.0.0` 2023-06-09
89
+ - 优化一系列问题
90
+ - orm 执行更优雅
91
+
92
+ `0.1.6` 2022-11-03
93
+ - orm 允许更新为空数据
94
+ - server 文件上传加入大小限制
95
+ - smtp 出错异常处理
96
+
97
+ `0.1.5` 2022-09-19
98
+ - 修复logging level 错误的问题
99
+ - 修正默认依赖
100
+
101
+ `0.1.4` 2022-09-12
102
+ - 优化orm 超时的问题
103
+ - 增加websocket 支持
104
+
105
+
106
+ ## Documentation
107
+
108
+
109
+ [Docs](https://cloudoll.chuchur.com)
110
+
111
+ [Docs](https://cloudoll.chuchur.com)
112
+
113
+ [Structure](https://cloudoll.chuchur.com/structure)
114
+
115
+ [Config](https://cloudoll.chuchur.com/config)
116
+
117
+ [Middleware](https://cloudoll.chuchur.com/middleware)
118
+
119
+ [Router](https://cloudoll.chuchur.com/router)
120
+
121
+ [View](https://cloudoll.chuchur.com/view)
122
+
123
+ [Cookie and Session](https://cloudoll.chuchur.com/session-cookie)
124
+
125
+ [Upload file](https://cloudoll.chuchur.com/file-uploads)
126
+
127
+ [WebSocket](https://cloudoll.chuchur.com/websockets)
128
+
129
+ [Mysql](https://cloudoll.chuchur.com/mysql)
130
+
131
+ [Deployment](https://cloudoll.chuchur.com/deployment)
132
+
133
+
134
+ ## 环境准备
135
+
136
+ - 操作系统:支持 macOS,Linux,Windows
137
+ - 运行环境:最低要求 3.6.0。
138
+
139
+ ## 快速开始
140
+
141
+ ```sh
142
+ $ mkdir cloudoll-demo && cd cloudoll-demo
143
+ $ pip3 install cloudoll
144
+ $ vi app.py
145
+ ```
146
+
147
+ `app.py` 内容如下:
148
+
149
+ ```python
150
+ ## /app.py
151
+ from cloudoll.web import app
152
+
153
+
154
+ if __name__ == "__main__":
155
+ app.create().run()
156
+ ```
157
+
158
+ ### 编写 Controller
159
+
160
+ ```sh
161
+ $ mkdir -p controllers/home
162
+ $ touch controllers/home/__init__.py
163
+ $ vi controllers/home/index.py
164
+ ```
165
+
166
+ `controllers/home/index.py` 内容如下:
167
+
168
+ ```python
169
+ # /controllers/home/index.py
170
+ from cloudoll.web import get
171
+
172
+ @get('/')
173
+ async def home():
174
+ return {"name": "chuchur" ,"msg": "ok"}
175
+ ```
176
+
177
+ 运行:
178
+ ```sh
179
+ $ python3 app.py
180
+ $ open http://localhost:9001
181
+ ```
182
+
183
+ 在浏览器打开 [http://127.0.0.1:9001/](http://127.0.0.1:9001/)
184
+
185
+ 就能看到:
186
+
187
+ ```json
188
+ {
189
+ "name": "chuchur" ,
190
+ "msg": "ok" ,
191
+ "timestamp": 1681993906410
192
+ }
193
+ ```
194
+
195
+ 恭喜, 你已经成功的写好了一个 `Restful API`接口.
196
+
197
+
198
+ ### 模板渲染
199
+
200
+ 绝大多数情况,我们都需要读取数据后渲染模板,然后呈现给用户。故我们需要引入对应的模板引擎。
201
+
202
+ 在本例中,我们使用 [Nunjucks](https://mozilla.github.io/nunjucks/) 来渲染
203
+ ```sh
204
+ $ mkdir templates
205
+ $ vi templates/index.html
206
+ ```
207
+
208
+ `index.html` 内容如下:
209
+
210
+ ```html
211
+ <!-- /templates/index.html -->
212
+
213
+ <!DOCTYPE html>
214
+ <html lang="zh">
215
+ <head>
216
+ <meta charset="UTF-8">
217
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
218
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
219
+ <title>Home</title>
220
+ </head>
221
+ <body>
222
+ <p>My name is {{name} }</p>
223
+ </body>
224
+ </html>
225
+ ```
226
+
227
+
228
+ 修改 `/controllers/home/index.py` 内容如下:
229
+
230
+ ```python
231
+ # /controllers/home/index.py
232
+ from cloudoll.web import get, render_view
233
+
234
+ @get('/')
235
+ async def home():
236
+ data = {"name": "chuchur" ,"msg": "ok"}
237
+ return render_view("index.html",data)
238
+ ```
239
+
240
+ 这时 页面正常渲染 ,可以看到 `“My name is chuchur”`
241
+
242
+ 恭喜, 你已经成功的写好了一个视图页面.
243
+
244
+ ### 静态资源
245
+
246
+ 我们想在模版里面嵌入静态资源,如图片,js ,css , 这个时候就得用到静态资源. 我们把这些`js` ,`css` ,`image` 都放到 `static` 目录
247
+
248
+ 线上环境建议部署到 CDN,或者使用 `nginx` 等相关服务器
249
+
250
+ ```sh
251
+ $ mkdir -p static/img
252
+ $ mkdir -p static/js
253
+ $ mkdir -p static/css
254
+ ```
255
+ 在 `img`目录 放入在张图 名`logo.png`
256
+
257
+ 在 `js` 目录新建 `index.js` ,内容如下:
258
+
259
+ 点击页面 弹出 "hello world"
260
+ ```js
261
+ // /static/js/index.js
262
+ document.addEventListener('DOMContentLoaded',function(){
263
+ document.body.addEventListener('click',function(){
264
+ alert('hello world.')
265
+ })
266
+ })
267
+ ```
268
+ 在 `css` 目录新建 `index.css` ,内容如下:
269
+ ```css
270
+ /* /static/css/index.css */
271
+ html,body {
272
+ width: 100%;
273
+ height: 100%;
274
+ color: red;
275
+ }
276
+ ```
277
+
278
+ 修改视图 `/templates/index.html` ,在 `head` 引入 静态资源, 内容如下:
279
+
280
+ ```html
281
+ <!-- /templates/index.html -->
282
+ <!DOCTYPE html>
283
+ <html lang="zh">
284
+ <head>
285
+ <meta charset="UTF-8">
286
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
287
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
288
+ <link rel="icon" type="image/png" href="/static/img/logo.png"/>
289
+ <link rel="stylesheet" href="/static/css/index.css">
290
+ <script src="/static/js/index.js"></script>
291
+ <title>Home</title>
292
+ </head>
293
+ <body>
294
+ <p>My name is {{name} }</p>
295
+ </body>
296
+ </html>
297
+ ```
298
+
299
+ 我们新建一个配置文件, 在配置文件里面配置静态 资源.
300
+
301
+ ```sh
302
+ $ mkdir config
303
+ $ vi config/conf.local.yaml
304
+ ```
305
+
306
+ `/config/conf.local.yaml` 内容如下:
307
+
308
+ ```yaml
309
+ server:
310
+ static:
311
+ prefix: /static
312
+ ```
313
+
314
+
315
+ 刷新页面之后, 我们所改动即可呈现.
316
+
317
+ ### 编写 Middleware
318
+
319
+ 假设有个需求:我们的新闻站点,禁止百度爬虫访问。
320
+
321
+ 所以可以通过 `Middleware` 判断 User-Agent,如下:
322
+
323
+ ```sh
324
+ $ mkdir middlewares
325
+ $ vi middlewares/robot.py
326
+ ```
327
+
328
+ 修改 `middlewares/robot.py`, 内容如下:
329
+
330
+ ```python
331
+ # /middlewares/robot.py
332
+ from cloudoll.web import middleware, render_json
333
+ import re
334
+
335
+ @middleware()
336
+ def mid_robot():
337
+ async def robot(request, handler):
338
+ ua = request.headers.get('user-agent')
339
+ if re.match('Baiduspider', ua):
340
+ return render_json(status=403, text="Go away , robot.")
341
+ return await handler(request)
342
+
343
+ return robot
344
+ ```
345
+
346
+ 重新启动之后, 现在可以使用 `curl http://localhost:7001/news -A "Baiduspider"` 看看效果。
347
+
348
+ 更多参见中间件文档。
349
+
350
+ ### 配置文件
351
+ 写业务的时候,不可避免的需要有配置文件,使用代码管理配置,在代码中添加多个环境的配置,在启动时传入当前环境的参数即可.
352
+
353
+ cloudoll 支持根据环境来加载配置,定义多个环境的配置文件
354
+
355
+ ```ini
356
+ config
357
+ |- conf.local.yaml
358
+ |- conf.prod.yaml
359
+ `- conf.test.yaml
360
+ ```
361
+
362
+ 我们创建配置文件:
363
+
364
+ ```sh
365
+ $ mkdir -p config/conf.local.yaml
366
+ $ vi config/conf.local.yaml
367
+ ```
368
+
369
+ 如下是 mysql 和 server 的配置:
370
+
371
+ ```yaml
372
+ server:
373
+ host: 192.168.0.1
374
+ port: 9001
375
+ static: false
376
+ client_max_size: 1024000
377
+ static:
378
+ prefix: /static
379
+ show_index: true
380
+ append_version: true
381
+ follow_symlinks: true
382
+ database:
383
+ mysql:
384
+ host: 127.0.0.1
385
+ port: 3306
386
+ user: root
387
+ password: abcd
388
+ db: blog
389
+ charset: utf8mb4
390
+ ```
391
+
392
+ 默认开发会使用默认的`local`作为配置。 启动时 通过 `env` 加载对应的配置。 如 `python3 app.py --env=prod` 会加载 `conf.prod.yaml`
393
+
394
+ # cli
395
+
396
+ ## 生成模型
397
+
398
+ 从数据库导出 `users` 表模型
399
+ ```sh
400
+ cloudoll gen -t users
401
+ ```
402
+ 更多参数 :
403
+ - -p (--path) 导出的模型路径
404
+ - -c (--create) 值 model(默认) 生成模型, 值table 建表
405
+ - -t (--table) 要生的模型或要建的表名,以`,`分开, `ALL` 所有表
406
+ - -env (--environment) 读取配置名 ,值 local(默认) / test / prod
407
+ - -db (--database) 数据库实例名,取决于配置文件,如果有多个数据库
408
+ - -h (--help) 帮助
409
+
410
+ ## 开发调试
411
+
412
+ ```sh
413
+ cloudoll start
414
+ ```