afwf_example 0.1.1__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.
- afwf_example/__init__.py +1 -0
- afwf_example/cache.py +11 -0
- afwf_example/cli.py +21 -0
- afwf_example/handlers/__init__.py +2 -0
- afwf_example/handlers/error.py +22 -0
- afwf_example/handlers/memorize_cache.py +43 -0
- afwf_example/handlers/open_file.py +49 -0
- afwf_example/handlers/open_url.py +48 -0
- afwf_example/handlers/open_url_new.py +51 -0
- afwf_example/handlers/read_file.py +44 -0
- afwf_example/handlers/set_settings.py +103 -0
- afwf_example/handlers/view_settings.py +33 -0
- afwf_example/handlers/write_file.py +91 -0
- afwf_example/paths.py +35 -0
- afwf_example/settings.py +12 -0
- afwf_example/vendor/__init__.py +2 -0
- afwf_example/vendor/pytest_cov_helper.py +125 -0
- afwf_example/workflow.py +26 -0
- afwf_example-0.1.1.dist-info/METADATA +103 -0
- afwf_example-0.1.1.dist-info/RECORD +25 -0
- afwf_example-0.1.1.dist-info/WHEEL +5 -0
- afwf_example-0.1.1.dist-info/entry_points.txt +2 -0
- afwf_example-0.1.1.dist-info/licenses/AUTHORS.rst +15 -0
- afwf_example-0.1.1.dist-info/licenses/LICENSE.txt +21 -0
- afwf_example-0.1.1.dist-info/top_level.txt +1 -0
afwf_example/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
afwf_example/cache.py
ADDED
afwf_example/cli.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Command:
|
|
7
|
+
def open_url_new(self, query: str = ""):
|
|
8
|
+
from afwf_example.handlers.open_url_new import handler
|
|
9
|
+
|
|
10
|
+
sf = handler.handler(query)
|
|
11
|
+
print(json.dumps(sf.to_script_filter(), indent=4))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
import fire
|
|
16
|
+
|
|
17
|
+
fire.Fire(Command)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
main()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
This handler will always raise an error. It is used for testing purpose.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import typing as T
|
|
8
|
+
import attrs
|
|
9
|
+
import afwf.api as afwf
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@attrs.define
|
|
13
|
+
class Handler(afwf.Handler):
|
|
14
|
+
def main(self) -> afwf.ScriptFilter:
|
|
15
|
+
afwf.log_debug_info("before raising the error")
|
|
16
|
+
raise Exception("raise this error intentionally")
|
|
17
|
+
|
|
18
|
+
def parse_query(self, query: str) -> T.Dict[str, T.Any]:
|
|
19
|
+
return {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
handler = Handler(id="error")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
[CN]
|
|
5
|
+
|
|
6
|
+
该 Script Filter 的功能是根据 key 随机生成一个 1 ~ 1000 之间的 value. 这个 value 的值
|
|
7
|
+
将会被缓存 5 秒. 5 秒内查询同一个 key 的结果将会是一样的. 该例子用来展示如何使用 time to live
|
|
8
|
+
缓存.
|
|
9
|
+
|
|
10
|
+
在 Alfred Workflow 的 Canvas 界面中 Script Filter 的设置如下:
|
|
11
|
+
|
|
12
|
+
- Keyword: afwf-example-memorize-cache, Argument Required
|
|
13
|
+
- Language: /bin/bash
|
|
14
|
+
- Script: python main.py 'memorize_cache {query}'
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import random
|
|
18
|
+
|
|
19
|
+
import attrs
|
|
20
|
+
import afwf.api as afwf
|
|
21
|
+
|
|
22
|
+
from ..cache import cache
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@attrs.define
|
|
26
|
+
class Handler(afwf.Handler):
|
|
27
|
+
@cache.memoize(tag="memorize_cache", expire=5)
|
|
28
|
+
def main(self, key: str) -> afwf.ScriptFilter:
|
|
29
|
+
sf = afwf.ScriptFilter()
|
|
30
|
+
value = random.randint(1, 1000)
|
|
31
|
+
item = afwf.Item(
|
|
32
|
+
title=f"value is {value}",
|
|
33
|
+
)
|
|
34
|
+
sf.items.append(item)
|
|
35
|
+
return sf
|
|
36
|
+
|
|
37
|
+
def parse_query(self, query: str):
|
|
38
|
+
return dict(
|
|
39
|
+
key=query,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
handler = Handler(id="memorize_cache")
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
[CN]
|
|
5
|
+
|
|
6
|
+
该 Script Filter 的功能是展示当前这个 handlers 文件夹下的所有 Python 文件供用户选择,
|
|
7
|
+
用户可以用上下选择文件, 也可以输入字符来过滤文件. 选中后按回车就会用默认应用打开对应的文件.
|
|
8
|
+
|
|
9
|
+
我们准备用 Alfred filters results 功能帮我们过滤文件, 所以我们无需在 main() 中接收参数,
|
|
10
|
+
免去了自己实现过滤文件的功能. 那么我们在实现 ``parse_query()`` 函数的时候直接返回空字典即可.
|
|
11
|
+
|
|
12
|
+
在 Alfred Workflow 的 Canvas 界面中 Script Filter 的设置如下:
|
|
13
|
+
|
|
14
|
+
- Keyword: afwf-example-open-file, Argument Optional
|
|
15
|
+
- Language: /bin/bash
|
|
16
|
+
- Script: python main.py 'open_file {query}'
|
|
17
|
+
- Alfred filters results: checked
|
|
18
|
+
- 连接一个 Utilities - Conditional 的控件, 条件是 ``{var:open_file}`` is equal to ``y``.
|
|
19
|
+
- 连接一个 Actions - Open File 的控件, File 的参数是 ``{var:open_file_path}``.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import attrs
|
|
23
|
+
from pathlib_mate import Path
|
|
24
|
+
import afwf.api as afwf
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@attrs.define
|
|
28
|
+
class Handler(afwf.Handler):
|
|
29
|
+
def main(self) -> afwf.ScriptFilter:
|
|
30
|
+
sf = afwf.ScriptFilter()
|
|
31
|
+
dir_here = Path.dir_here(__file__)
|
|
32
|
+
for p in dir_here.iterdir():
|
|
33
|
+
if p.ext.lower() == ".py":
|
|
34
|
+
item = afwf.Item(
|
|
35
|
+
title=p.basename,
|
|
36
|
+
subtitle=f"Open {p.abspath}",
|
|
37
|
+
autocomplete=p.basename,
|
|
38
|
+
match=p.basename,
|
|
39
|
+
arg=p.abspath,
|
|
40
|
+
)
|
|
41
|
+
item.open_file(path=p.abspath)
|
|
42
|
+
sf.items.append(item)
|
|
43
|
+
return sf
|
|
44
|
+
|
|
45
|
+
def parse_query(self, query: str):
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
handler = Handler(id="open_file")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
[CN]
|
|
5
|
+
|
|
6
|
+
该 Script Filter 的功能是展示一些预先定义好的网站的名字和 URL. 然后选中后按回车就会在浏览器
|
|
7
|
+
内打开对应网站.
|
|
8
|
+
|
|
9
|
+
这个 Script Filter 没有输入参数. 所以 ``main()`` 函数也没有参数. 那么我们在实现
|
|
10
|
+
``parse_query()`` 函数的时候直接返回空字典即可.
|
|
11
|
+
|
|
12
|
+
在 Alfred Workflow 的 Canvas 界面中 Script Filter 的设置如下:
|
|
13
|
+
|
|
14
|
+
- Keyword: afwf-example-open-url, No Argument
|
|
15
|
+
- Language: /bin/bash
|
|
16
|
+
- Script: python main.py 'open_url {query}', 这里我们没有勾选 Alfred filters results. 因为我们不需要 Alfred 帮我们过滤结果.
|
|
17
|
+
- 连接一个 Utilities - Conditional 的控件, 条件是 ``{var:open_url}`` is equal to ``y``.
|
|
18
|
+
- 连接一个 Actions - Open Url 的控件, URL 的参数是 ``{var:open_url_arg}``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import attrs
|
|
22
|
+
import afwf.api as afwf
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@attrs.define
|
|
26
|
+
class Handler(afwf.Handler):
|
|
27
|
+
def main(self) -> afwf.ScriptFilter:
|
|
28
|
+
sf = afwf.ScriptFilter()
|
|
29
|
+
for title, url in [
|
|
30
|
+
("Alfred App", "https://www.alfredapp.com/"),
|
|
31
|
+
("Python", "https://www.python.org/"),
|
|
32
|
+
("GitHub", "https://github.com/"),
|
|
33
|
+
]:
|
|
34
|
+
item = afwf.Item(
|
|
35
|
+
title=title,
|
|
36
|
+
subtitle=f"open {url}",
|
|
37
|
+
autocomplete=title,
|
|
38
|
+
arg=url,
|
|
39
|
+
)
|
|
40
|
+
item.open_url(url=url)
|
|
41
|
+
sf.items.append(item)
|
|
42
|
+
return sf
|
|
43
|
+
|
|
44
|
+
def parse_query(self, query: str):
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
handler = Handler(id="open_url")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
[CN]
|
|
5
|
+
|
|
6
|
+
该 Script Filter 的功能是展示一些预先定义好的网站的名字和 URL. 然后选中后按回车就会在浏览器
|
|
7
|
+
内打开对应网站.
|
|
8
|
+
|
|
9
|
+
这个 Script Filter 没有输入参数. 所以 ``main()`` 函数也没有参数. 那么我们在实现
|
|
10
|
+
``parse_query()`` 函数的时候直接返回空字典即可, ``encode_query()`` 也直接返回空字符串.
|
|
11
|
+
|
|
12
|
+
在 Alfred Workflow 的 Canvas 界面中 Script Filter 的设置如下:
|
|
13
|
+
|
|
14
|
+
- Keyword: afwf-example-open-url, No Argument
|
|
15
|
+
- Language: /bin/bash
|
|
16
|
+
- Script: python main.py 'open_url {query}', 这里我们没有勾选 Alfred filters results. 因为我们不需要 Alfred 帮我们过滤结果.
|
|
17
|
+
- 连接一个 Utilities - Conditional 的控件, 条件是 ``{var:open_url}`` is equal to ``y``.
|
|
18
|
+
- 连接一个 Actions - Open Url 的控件, URL 的参数是 ``{var:open_url_arg}``.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from afwf.handler import Handler
|
|
22
|
+
from afwf.script_filter import ScriptFilter
|
|
23
|
+
from afwf.item import Item
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OpenUrlHandler(Handler):
|
|
27
|
+
def parse_query(self, query: str) -> dict:
|
|
28
|
+
return {}
|
|
29
|
+
|
|
30
|
+
def encode_query(self, **kwargs) -> str:
|
|
31
|
+
return ""
|
|
32
|
+
|
|
33
|
+
def main(self) -> ScriptFilter:
|
|
34
|
+
sf = ScriptFilter()
|
|
35
|
+
for title, url in [
|
|
36
|
+
("Alfred App", "https://www.alfredapp.com/"),
|
|
37
|
+
("Python", "https://www.python.org/"),
|
|
38
|
+
("GitHub", "https://github.com/"),
|
|
39
|
+
]:
|
|
40
|
+
item = Item(
|
|
41
|
+
title=title,
|
|
42
|
+
subtitle=f"open {url}",
|
|
43
|
+
autocomplete=title,
|
|
44
|
+
arg=url,
|
|
45
|
+
)
|
|
46
|
+
item.open_url(url=url)
|
|
47
|
+
sf.items.append(item)
|
|
48
|
+
return sf
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
handler = OpenUrlHandler(id="open_url")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
[CN]
|
|
5
|
+
|
|
6
|
+
该 Script Filter 的功能是展示 file.txt 文件中的内容. 仅仅是和 ``write_file.py`` 模块
|
|
7
|
+
配合使用, 永远验证.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import attrs
|
|
11
|
+
import afwf.api as afwf
|
|
12
|
+
|
|
13
|
+
from ..paths import dir_project_home
|
|
14
|
+
|
|
15
|
+
path_file = dir_project_home / "file.txt"
|
|
16
|
+
path_file.parent.mkdir(parents=True, exist_ok=True)
|
|
17
|
+
if path_file.exists() is False:
|
|
18
|
+
path_file.write_text("hello world")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@attrs.define
|
|
22
|
+
class Handler(afwf.Handler):
|
|
23
|
+
def main(self) -> afwf.ScriptFilter:
|
|
24
|
+
sf = afwf.ScriptFilter()
|
|
25
|
+
if path_file.exists():
|
|
26
|
+
content = path_file.read_text()
|
|
27
|
+
item = afwf.Item(
|
|
28
|
+
title=f"content of {path_file} is",
|
|
29
|
+
subtitle=content,
|
|
30
|
+
)
|
|
31
|
+
else:
|
|
32
|
+
item = afwf.Item(
|
|
33
|
+
title=f"{path_file} does not exist!",
|
|
34
|
+
)
|
|
35
|
+
item.set_icon(afwf.IconFileEnum.error)
|
|
36
|
+
|
|
37
|
+
sf.items.append(item)
|
|
38
|
+
return sf
|
|
39
|
+
|
|
40
|
+
def parse_query(self, query: str):
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
handler = Handler(id="read_file")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
[CN]
|
|
5
|
+
|
|
6
|
+
该 Script Filter 的功能是让用户对用作 settings 的 sqlite 写入. 可以和 ``view_settings.py``
|
|
7
|
+
模块配合使用查看效果.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import typing as T
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import attrs
|
|
14
|
+
import afwf.api as afwf
|
|
15
|
+
|
|
16
|
+
from ..settings import settings, SettingsKeyEnum
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@attrs.define
|
|
20
|
+
class SetSettingValueHandler(afwf.Handler):
|
|
21
|
+
def main(self, key: str, value: str) -> afwf.ScriptFilter:
|
|
22
|
+
sf = afwf.ScriptFilter()
|
|
23
|
+
settings[key] = value
|
|
24
|
+
return sf
|
|
25
|
+
|
|
26
|
+
def parse_query(self, query: str):
|
|
27
|
+
key, value = query.split(" ", 1)
|
|
28
|
+
return dict(
|
|
29
|
+
key=key,
|
|
30
|
+
value=value,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def encode_query(self, key: str, value: str) -> str:
|
|
34
|
+
return f"{key} {value}"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
set_setting_value_handler = SetSettingValueHandler(id="set_setting_value")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@attrs.define
|
|
41
|
+
class Handler(afwf.Handler):
|
|
42
|
+
def main(
|
|
43
|
+
self,
|
|
44
|
+
key: T.Optional[str] = None,
|
|
45
|
+
value: T.Optional[str] = None,
|
|
46
|
+
) -> afwf.ScriptFilter:
|
|
47
|
+
sf = afwf.ScriptFilter()
|
|
48
|
+
|
|
49
|
+
if key is None:
|
|
50
|
+
for settings_key in SettingsKeyEnum:
|
|
51
|
+
item = afwf.FuzzyItem(
|
|
52
|
+
title=settings_key.value,
|
|
53
|
+
subtitle=f"set {settings_key.value} to ...",
|
|
54
|
+
autocomplete=settings_key.value + " ",
|
|
55
|
+
).set_fuzzy_match_name(settings_key.value)
|
|
56
|
+
sf.items.append(item)
|
|
57
|
+
elif value is None:
|
|
58
|
+
items = list()
|
|
59
|
+
for settings_key in SettingsKeyEnum:
|
|
60
|
+
item = afwf.FuzzyItem(
|
|
61
|
+
title=settings_key.value,
|
|
62
|
+
subtitle=f"set {settings_key.value} to ...",
|
|
63
|
+
autocomplete=settings_key.value + " ",
|
|
64
|
+
).set_fuzzy_match_name(settings_key.value)
|
|
65
|
+
items.append(item)
|
|
66
|
+
matcher = afwf.FuzzyItemMatcher.from_items(items)
|
|
67
|
+
sf.items.extend(matcher.match(key, threshold=0))
|
|
68
|
+
else:
|
|
69
|
+
if key in SettingsKeyEnum.__members__:
|
|
70
|
+
item = afwf.Item(
|
|
71
|
+
title=f"Set settings.{key} = {value!r}",
|
|
72
|
+
)
|
|
73
|
+
item.send_notification(
|
|
74
|
+
title=f"Set settings.{key} = {value!r}",
|
|
75
|
+
)
|
|
76
|
+
cmd = set_setting_value_handler.encode_run_script_command(
|
|
77
|
+
bin_python=sys.executable,
|
|
78
|
+
key=key,
|
|
79
|
+
value=value,
|
|
80
|
+
)
|
|
81
|
+
item.run_script(cmd)
|
|
82
|
+
sf.items.append(item)
|
|
83
|
+
else:
|
|
84
|
+
item = afwf.Item(
|
|
85
|
+
title=f"{key!r} is not a valid settings key",
|
|
86
|
+
)
|
|
87
|
+
item.set_icon(afwf.IconFileEnum.error)
|
|
88
|
+
sf.items.append(item)
|
|
89
|
+
return sf
|
|
90
|
+
|
|
91
|
+
def parse_query(self, query: str):
|
|
92
|
+
q = afwf.Query.from_str(query)
|
|
93
|
+
if q.n_trimmed_parts == 0:
|
|
94
|
+
return dict(key=None, value=None)
|
|
95
|
+
elif q.n_trimmed_parts == 1:
|
|
96
|
+
return dict(key=q.trimmed_parts[0], value=None)
|
|
97
|
+
elif q.n_trimmed_parts == 2:
|
|
98
|
+
return dict(key=q.trimmed_parts[0], value=q.trimmed_parts[1])
|
|
99
|
+
else:
|
|
100
|
+
raise NotImplementedError
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
handler = Handler(id="set_settings")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
[CN]
|
|
5
|
+
|
|
6
|
+
该 Script Filter 的功能是让用户对用作 settings 的 sqlite 进行读取. 可以和 ``set_settings.py``
|
|
7
|
+
模块配合使用查看效果.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import attrs
|
|
11
|
+
import afwf.api as afwf
|
|
12
|
+
|
|
13
|
+
from ..settings import path_settings_sqlite, settings, SettingsKeyEnum
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@attrs.define
|
|
17
|
+
class Handler(afwf.Handler):
|
|
18
|
+
def main(self) -> afwf.ScriptFilter:
|
|
19
|
+
sf = afwf.ScriptFilter()
|
|
20
|
+
for settings_key in SettingsKeyEnum:
|
|
21
|
+
value = settings.get(settings_key.value)
|
|
22
|
+
item = afwf.Item(
|
|
23
|
+
title=f"settings.{settings_key} = {value!r}",
|
|
24
|
+
subtitle=f"settings are stored at {path_settings_sqlite}",
|
|
25
|
+
)
|
|
26
|
+
sf.items.append(item)
|
|
27
|
+
return sf
|
|
28
|
+
|
|
29
|
+
def parse_query(self, query: str):
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
handler = Handler(id="view_settings")
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
[CN]
|
|
5
|
+
|
|
6
|
+
该 Script Filter 的功能是让用户将文本写入特定文件. 该例子用来展示如何实现按下回车后执行任意
|
|
7
|
+
Python 逻辑. 该技巧可以用来实现按下回车后做到任何事情, 因为底层就是一个 Python 脚本.
|
|
8
|
+
|
|
9
|
+
其原理如下:
|
|
10
|
+
|
|
11
|
+
我们这里多了一个 ``WriteRequestHandler`` 的 handler. 这个 handler 并不会跟任何
|
|
12
|
+
Script Filter 所对应, 而是仅仅作为一个 CLI 命令行接口而存在. 这样我们在其他的 handler 中
|
|
13
|
+
只要将 item 之后的 action 定义为 Run Script, 那么就可以按下回车后用 bash 来执行任何
|
|
14
|
+
Python 逻辑. 而这个 bash command 就是 item 的 argument. 这个 ``WriteRequestHandler``
|
|
15
|
+
则是实现了 bash command 所对应的功能.
|
|
16
|
+
|
|
17
|
+
``WriteRequestHandler`` 实现了将任意 content 写入 file.txt 的功能. 并且在
|
|
18
|
+
``encode_query()`` 中实现了如何将 main 中的参数编码成字符串的函数.
|
|
19
|
+
|
|
20
|
+
``Handler`` 则实现了 Script Filter 的功能.
|
|
21
|
+
|
|
22
|
+
在 Alfred Workflow 的 Canvas 界面中 Script Filter 的设置如下:
|
|
23
|
+
|
|
24
|
+
- Keyword: afwf-example-write-file, Argument Required
|
|
25
|
+
- Language: /bin/bash
|
|
26
|
+
- Script: python main.py 'write_file {query}'
|
|
27
|
+
- 连接一个 Utilities - Conditional 的控件, 条件是 ``{var:run_script}`` is equal to ``y``.
|
|
28
|
+
- 连接一个 Actions - Run Script 的控件, Script 的参数是 ``{query}``.
|
|
29
|
+
- 连接一个 Utilities - Conditional 的控件, 条件是 ``{var:send_notification}`` is equal to ``y``.
|
|
30
|
+
- 连接一个 Outputs - Post Notification 的控件, Title 的参数是 ``{var:send_notification_title}``
|
|
31
|
+
Subtitle 的参数是 ``{var:send_notification_subtitle}``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import sys
|
|
35
|
+
|
|
36
|
+
import attrs
|
|
37
|
+
import afwf.api as afwf
|
|
38
|
+
|
|
39
|
+
from ..paths import dir_project_home
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@attrs.define
|
|
43
|
+
class WriteRequestHandler(afwf.Handler):
|
|
44
|
+
def main(self, content: str) -> afwf.ScriptFilter:
|
|
45
|
+
sf = afwf.ScriptFilter()
|
|
46
|
+
path = dir_project_home / "file.txt"
|
|
47
|
+
path.write_text(content)
|
|
48
|
+
return sf
|
|
49
|
+
|
|
50
|
+
def parse_query(self, query: str):
|
|
51
|
+
return dict(
|
|
52
|
+
content=query,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def encode_query(self, content: str) -> str:
|
|
56
|
+
return content
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
write_request_handler = WriteRequestHandler(id="write_request_handler")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
path_file = dir_project_home / "file.txt"
|
|
63
|
+
path_file.parent.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@attrs.define
|
|
67
|
+
class Handler(afwf.Handler):
|
|
68
|
+
def main(self, content: str) -> afwf.ScriptFilter:
|
|
69
|
+
sf = afwf.ScriptFilter()
|
|
70
|
+
item = afwf.Item(
|
|
71
|
+
title=f"Write {content!r} to {path_file}",
|
|
72
|
+
)
|
|
73
|
+
cmd = write_request_handler.encode_run_script_command(
|
|
74
|
+
bin_python=sys.executable,
|
|
75
|
+
content=content,
|
|
76
|
+
)
|
|
77
|
+
item.run_script(cmd)
|
|
78
|
+
item.send_notification(
|
|
79
|
+
title=f"Write {content!r} to {path_file}",
|
|
80
|
+
subtitle="success",
|
|
81
|
+
)
|
|
82
|
+
sf.items.append(item)
|
|
83
|
+
return sf
|
|
84
|
+
|
|
85
|
+
def parse_query(self, query: str):
|
|
86
|
+
return dict(
|
|
87
|
+
content=query,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
handler = Handler(id="write_file")
|
afwf_example/paths.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from pathlib_mate import Path
|
|
4
|
+
|
|
5
|
+
dir_python_lib = Path.dir_here(__file__)
|
|
6
|
+
dir_project_root = dir_python_lib.parent
|
|
7
|
+
|
|
8
|
+
PACKAGE_NAME = dir_python_lib.basename
|
|
9
|
+
|
|
10
|
+
# ------------------------------------------------------------------------------
|
|
11
|
+
# Alfred Related
|
|
12
|
+
# ------------------------------------------------------------------------------
|
|
13
|
+
dir_home = Path.home()
|
|
14
|
+
dir_project_home = dir_home / ".alfred-afwf" / PACKAGE_NAME
|
|
15
|
+
dir_project_home.mkdir_if_not_exists()
|
|
16
|
+
|
|
17
|
+
dir_cache = dir_project_home / ".cache"
|
|
18
|
+
path_settings_sqlite = dir_project_home / "settings.sqlite"
|
|
19
|
+
|
|
20
|
+
path_config_json = dir_project_home / "config.json"
|
|
21
|
+
|
|
22
|
+
# ------------------------------------------------------------------------------
|
|
23
|
+
# Virtual Environment Related
|
|
24
|
+
# ------------------------------------------------------------------------------
|
|
25
|
+
dir_venv = dir_project_root / ".venv"
|
|
26
|
+
dir_venv_bin = dir_venv / "bin"
|
|
27
|
+
|
|
28
|
+
# virtualenv executable paths
|
|
29
|
+
bin_pytest = dir_venv_bin / "pytest"
|
|
30
|
+
|
|
31
|
+
# test related
|
|
32
|
+
dir_htmlcov = dir_project_root / "htmlcov"
|
|
33
|
+
path_cov_index_html = dir_htmlcov / "index.html"
|
|
34
|
+
dir_unit_test = dir_project_root / "tests"
|
|
35
|
+
dir_int_test = dir_project_root / "tests_int"
|
afwf_example/settings.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
from sqlitedict import SqliteDict
|
|
5
|
+
|
|
6
|
+
from .paths import path_settings_sqlite
|
|
7
|
+
|
|
8
|
+
settings = SqliteDict(path_settings_sqlite.abspath, autocommit=True)
|
|
9
|
+
|
|
10
|
+
class SettingsKeyEnum(str, enum.Enum):
|
|
11
|
+
email = "email"
|
|
12
|
+
password = "password"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import contextlib
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@contextlib.contextmanager
|
|
11
|
+
def temp_cwd(path: Path):
|
|
12
|
+
"""
|
|
13
|
+
Temporarily set the current working directory (CWD) and automatically
|
|
14
|
+
switch back when it's done.
|
|
15
|
+
"""
|
|
16
|
+
cwd = os.getcwd()
|
|
17
|
+
os.chdir(str(path))
|
|
18
|
+
try:
|
|
19
|
+
yield path
|
|
20
|
+
finally:
|
|
21
|
+
os.chdir(cwd)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_cov_test(
|
|
25
|
+
script: str,
|
|
26
|
+
module: str,
|
|
27
|
+
root_dir: str,
|
|
28
|
+
htmlcov_dir: str,
|
|
29
|
+
preview: bool = False,
|
|
30
|
+
is_folder: bool = False,
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
The pytest-cov plugin gives you the coverage for entire project. What if
|
|
34
|
+
I want run per-module test independently and get per-module coverage?
|
|
35
|
+
|
|
36
|
+
This is a simple wrapper around pytest + coverage cli command. Allow you to run
|
|
37
|
+
coverage test from Python script and set the code coverage measurement scope.
|
|
38
|
+
|
|
39
|
+
Usage example:
|
|
40
|
+
|
|
41
|
+
suppose you have a source code folder structure like this::
|
|
42
|
+
|
|
43
|
+
/dir_git_repo/
|
|
44
|
+
/dir_git_repo/my_library
|
|
45
|
+
/dir_git_repo/my_library/__init__.py
|
|
46
|
+
/dir_git_repo/my_library/module1.py
|
|
47
|
+
/dir_git_repo/my_library/module2.py
|
|
48
|
+
|
|
49
|
+
In your module 1 unit test script, you can do this:
|
|
50
|
+
|
|
51
|
+
.. code-block:: python
|
|
52
|
+
|
|
53
|
+
from my_library.module1 import func1, func2
|
|
54
|
+
|
|
55
|
+
def test_func1():
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def test_func2():
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
if __name__ == "__main__":
|
|
62
|
+
from fixa.pytest_cov_helper import run_cov_test
|
|
63
|
+
|
|
64
|
+
run_cov_test(
|
|
65
|
+
script=__file__,
|
|
66
|
+
module="my_library.module1", # test scope is the module1.py
|
|
67
|
+
root_dir="/path/to/dir_git_repo",
|
|
68
|
+
htmlcov_dir="/path/to/dir_git_repo/htmlcov",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
In your all modules unit test script, you can do this:
|
|
72
|
+
|
|
73
|
+
.. code-block:: python
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
from fixa.pytest_cov_helper import run_cov_test
|
|
77
|
+
|
|
78
|
+
run_cov_test(
|
|
79
|
+
script=__file__,
|
|
80
|
+
module="my_library", # test scope is the my_library/
|
|
81
|
+
root_dir="/path/to/dir_git_repo",
|
|
82
|
+
htmlcov_dir="/path/to/dir_git_repo/htmlcov",
|
|
83
|
+
is_folder=True, # my_library is a folder
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
:param script: the path to test script
|
|
87
|
+
:param module: the dot notation to the python module you want to calculate
|
|
88
|
+
coverage
|
|
89
|
+
:param root_dir: the dir to dump coverage results binary file
|
|
90
|
+
:param htmlcov_dir: the dir to dump HTML output
|
|
91
|
+
:param preview: whether to open the HTML output in web browser after the test
|
|
92
|
+
:param is_folder: whether the module is a folder
|
|
93
|
+
|
|
94
|
+
Reference:
|
|
95
|
+
|
|
96
|
+
- https://pypi.org/project/pytest-cov/
|
|
97
|
+
"""
|
|
98
|
+
bin_pytest = Path(sys.executable).parent / "pytest"
|
|
99
|
+
if is_folder:
|
|
100
|
+
script = f"{Path(script).parent}"
|
|
101
|
+
if module.endswith(".py"): # pragma: no cover
|
|
102
|
+
module = module[:-3]
|
|
103
|
+
args = [
|
|
104
|
+
f"{bin_pytest}",
|
|
105
|
+
"-s",
|
|
106
|
+
"--tb=native",
|
|
107
|
+
f"--rootdir={root_dir}",
|
|
108
|
+
f"--cov={module}",
|
|
109
|
+
"--cov-report",
|
|
110
|
+
"term-missing",
|
|
111
|
+
"--cov-report",
|
|
112
|
+
f"html:{htmlcov_dir}",
|
|
113
|
+
script,
|
|
114
|
+
]
|
|
115
|
+
with temp_cwd(Path(root_dir)):
|
|
116
|
+
subprocess.run(args)
|
|
117
|
+
if preview: # pragma: no cover
|
|
118
|
+
platform = sys.platform
|
|
119
|
+
if platform in ["win32", "cygwin"]:
|
|
120
|
+
open_command = "start"
|
|
121
|
+
elif platform in ["darwin", "linux"]:
|
|
122
|
+
open_command = "open"
|
|
123
|
+
else:
|
|
124
|
+
raise NotImplementedError
|
|
125
|
+
subprocess.run([open_command, f"{Path(htmlcov_dir).joinpath('index.html')}"])
|
afwf_example/workflow.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import afwf
|
|
4
|
+
|
|
5
|
+
from .handlers import (
|
|
6
|
+
open_url,
|
|
7
|
+
open_file,
|
|
8
|
+
write_file,
|
|
9
|
+
read_file,
|
|
10
|
+
error,
|
|
11
|
+
memorize_cache,
|
|
12
|
+
set_settings,
|
|
13
|
+
view_settings,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
wf = afwf.Workflow()
|
|
17
|
+
wf.register(open_url.handler)
|
|
18
|
+
wf.register(open_file.handler)
|
|
19
|
+
wf.register(write_file.write_request_handler)
|
|
20
|
+
wf.register(write_file.handler)
|
|
21
|
+
wf.register(read_file.handler)
|
|
22
|
+
wf.register(error.handler)
|
|
23
|
+
wf.register(memorize_cache.handler)
|
|
24
|
+
wf.register(set_settings.handler)
|
|
25
|
+
wf.register(set_settings.set_setting_value_handler)
|
|
26
|
+
wf.register(view_settings.handler)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: afwf_example
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A sample Alfred Workflow Python project.
|
|
5
|
+
Author-email: Sanhe Hu <husanhe@email.com>
|
|
6
|
+
Maintainer-email: Sanhe Hu <husanhe@email.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/MacHu-GWU/afwf_example-project
|
|
9
|
+
Project-URL: Documentation, https://afwf-example.readthedocs.io/en/latest/
|
|
10
|
+
Project-URL: Repository, https://github.com/MacHu-GWU/afwf_example-project
|
|
11
|
+
Project-URL: Issues, https://github.com/MacHu-GWU/afwf_example-project/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/MacHu-GWU/afwf_example-project/blob/main/release-history.rst
|
|
13
|
+
Project-URL: Download, https://pypi.org/pypi/afwf-example#files
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Operating System :: MacOS
|
|
19
|
+
Classifier: Operating System :: Unix
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Requires-Python: <4.0,>=3.10
|
|
27
|
+
Description-Content-Type: text/x-rst
|
|
28
|
+
License-File: LICENSE.txt
|
|
29
|
+
License-File: AUTHORS.rst
|
|
30
|
+
Requires-Dist: afwf<2.0.0,>=1.0.1
|
|
31
|
+
Requires-Dist: fire<1.0.0,>=0.7.0
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: rich<14.0.0,>=13.8.1; extra == "dev"
|
|
34
|
+
Provides-Extra: test
|
|
35
|
+
Requires-Dist: pytest<9.0.0,>=8.2.2; extra == "test"
|
|
36
|
+
Requires-Dist: pytest-cov<7.0.0,>=6.0.0; extra == "test"
|
|
37
|
+
Provides-Extra: doc
|
|
38
|
+
Requires-Dist: Sphinx<8.0.0,>=7.4.7; extra == "doc"
|
|
39
|
+
Requires-Dist: sphinx-copybutton<1.0.0,>=0.5.2; extra == "doc"
|
|
40
|
+
Requires-Dist: sphinx-design<1.0.0,>=0.6.1; extra == "doc"
|
|
41
|
+
Requires-Dist: sphinx-jinja<3.0.0,>=2.0.2; extra == "doc"
|
|
42
|
+
Requires-Dist: furo==2024.8.6; extra == "doc"
|
|
43
|
+
Requires-Dist: pygments<3.0.0,>=2.18.0; extra == "doc"
|
|
44
|
+
Requires-Dist: ipython<8.19.0,>=8.18.1; extra == "doc"
|
|
45
|
+
Requires-Dist: nbsphinx<1.0.0,>=0.8.12; extra == "doc"
|
|
46
|
+
Requires-Dist: rstobj==2.0.0; extra == "doc"
|
|
47
|
+
Requires-Dist: docfly==3.0.3; extra == "doc"
|
|
48
|
+
Provides-Extra: mise
|
|
49
|
+
Requires-Dist: PyGithub<3.0.0,>=2.8.0; extra == "mise"
|
|
50
|
+
Requires-Dist: httpx<1.0.0,>=0.28.0; extra == "mise"
|
|
51
|
+
Requires-Dist: tomli<3.0.0,>=2.0.0; python_version < "3.11" and extra == "mise"
|
|
52
|
+
Dynamic: license-file
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
.. image:: https://readthedocs.org/projects/afwf-example/badge/?version=latest
|
|
56
|
+
:target: https://afwf-example.readthedocs.io/en/latest/
|
|
57
|
+
:alt: Documentation Status
|
|
58
|
+
|
|
59
|
+
.. image:: https://github.com/MacHu-GWU/afwf_example-project/actions/workflows/main.yml/badge.svg
|
|
60
|
+
:target: https://github.com/MacHu-GWU/afwf_example-project/actions?query=workflow:CI
|
|
61
|
+
|
|
62
|
+
.. image:: https://codecov.io/gh/MacHu-GWU/afwf_example-project/branch/main/graph/badge.svg
|
|
63
|
+
:target: https://codecov.io/gh/MacHu-GWU/afwf_example-project
|
|
64
|
+
|
|
65
|
+
.. image:: https://img.shields.io/pypi/v/afwf-example.svg
|
|
66
|
+
:target: https://pypi.python.org/pypi/afwf-example
|
|
67
|
+
|
|
68
|
+
.. image:: https://img.shields.io/pypi/l/afwf-example.svg
|
|
69
|
+
:target: https://pypi.python.org/pypi/afwf-example
|
|
70
|
+
|
|
71
|
+
.. image:: https://img.shields.io/pypi/pyversions/afwf-example.svg
|
|
72
|
+
:target: https://pypi.python.org/pypi/afwf-example
|
|
73
|
+
|
|
74
|
+
.. image:: https://img.shields.io/badge/✍️_Release_History!--None.svg?style=social&logo=github
|
|
75
|
+
:target: https://github.com/MacHu-GWU/afwf_example-project/blob/main/release-history.rst
|
|
76
|
+
|
|
77
|
+
.. image:: https://img.shields.io/badge/⭐_Star_me_on_GitHub!--None.svg?style=social&logo=github
|
|
78
|
+
:target: https://github.com/MacHu-GWU/afwf_example-project
|
|
79
|
+
|
|
80
|
+
------
|
|
81
|
+
|
|
82
|
+
.. image:: https://img.shields.io/badge/Link-API-blue.svg
|
|
83
|
+
:target: https://afwf-example.readthedocs.io/en/latest/py-modindex.html
|
|
84
|
+
|
|
85
|
+
.. image:: https://img.shields.io/badge/Link-GitHub-blue.svg
|
|
86
|
+
:target: https://github.com/MacHu-GWU/afwf_example-project
|
|
87
|
+
|
|
88
|
+
.. image:: https://img.shields.io/badge/Link-Submit_Issue-blue.svg
|
|
89
|
+
:target: https://github.com/MacHu-GWU/afwf_example-project/issues
|
|
90
|
+
|
|
91
|
+
.. image:: https://img.shields.io/badge/Link-Request_Feature-blue.svg
|
|
92
|
+
:target: https://github.com/MacHu-GWU/afwf_example-project/issues
|
|
93
|
+
|
|
94
|
+
.. image:: https://img.shields.io/badge/Link-Download-blue.svg
|
|
95
|
+
:target: https://pypi.org/pypi/afwf-example#files
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
Welcome to ``afwf_example`` Documentation
|
|
99
|
+
==============================================================================
|
|
100
|
+
.. image:: https://afwf-example.readthedocs.io/en/latest/_static/afwf_example-logo.png
|
|
101
|
+
:target: https://afwf-example.readthedocs.io/en/latest/
|
|
102
|
+
|
|
103
|
+
This project demonstrates best practices for building Alfred Workflows using the `afwf <https://github.com/MacHu-GWU/afwf-project>`_ framework. You can use it as a reference to learn how ``afwf`` works. A `cookiecutter-afwf <https://github.com/MacHu-GWU/cookiecutter-afwf>`_ project template is also available — simply provide a project name and it will generate a ready-to-use Git repository with all the automation scripts and example code you need.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
afwf_example/__init__.py,sha256=iwhKnzeBJLKxpRVjvzwiRE63_zNpIBfaKLITauVph-0,24
|
|
2
|
+
afwf_example/cache.py,sha256=uNlhEjtVE3_Gct0NgZLgMSl-XPZicd03VOCIDiUabEU,158
|
|
3
|
+
afwf_example/cli.py,sha256=gsm0dRrnFH6H4n2F_zZKmzV3pp2c8WvQ4s5diWwXpxc,352
|
|
4
|
+
afwf_example/paths.py,sha256=_dmFHiHXx9jTW9GaUjqQLb1Csdca9glNUHR_vqGqN_I,1162
|
|
5
|
+
afwf_example/settings.py,sha256=ioVAV9BmKLKU4UGM9sq5aoFAZzejVSOgMFEt5ncNBW8,268
|
|
6
|
+
afwf_example/workflow.py,sha256=w_v_lDFlF_BZVS7yygj5ahHlrbeEwotrsucyfPVjoV4,568
|
|
7
|
+
afwf_example/handlers/__init__.py,sha256=O9CT1B2F-cVB8elT0EoCJbgkcffjvlmqteqavs4giDg,25
|
|
8
|
+
afwf_example/handlers/error.py,sha256=S9pW5YqR2hGFUIO3k7vgN_D2Vl3JJk3JISYOOoiRy8I,475
|
|
9
|
+
afwf_example/handlers/memorize_cache.py,sha256=rbel01gHmVCVhSe1gRQViy8DNh8u7WR8pU9uDzp_qqM,1042
|
|
10
|
+
afwf_example/handlers/open_file.py,sha256=UzTBP2IJFSJNTCLmQfrf7LPXBeDRReVrdLp_cASFLSw,1706
|
|
11
|
+
afwf_example/handlers/open_url.py,sha256=uEL2Y_5p9-_-DNFB1pPYmDSo-1rfexZVDrbizKF8pB8,1534
|
|
12
|
+
afwf_example/handlers/open_url_new.py,sha256=vEQkd8wxQ8CgiqGwkJ76prQmKupjg4N5Ik5KFDoxC7Y,1701
|
|
13
|
+
afwf_example/handlers/read_file.py,sha256=fWlKcpIvll-cokpuKy9AsTpuePIFEaArC_YaEEeKFOQ,1044
|
|
14
|
+
afwf_example/handlers/set_settings.py,sha256=TSnJj1JfRvaoCxL0Eu3vAkqU7AFNvBWo_pHeJMj0Fag,3227
|
|
15
|
+
afwf_example/handlers/view_settings.py,sha256=KtCeSzDkFxNY-leHZaTo8C3ctvcdMgJ55a24SNfNKzk,842
|
|
16
|
+
afwf_example/handlers/write_file.py,sha256=fLA9GvSagPHspt25T2Pv8acsIBGzaO0iOwgT-M1AGBw,3027
|
|
17
|
+
afwf_example/vendor/__init__.py,sha256=O9CT1B2F-cVB8elT0EoCJbgkcffjvlmqteqavs4giDg,25
|
|
18
|
+
afwf_example/vendor/pytest_cov_helper.py,sha256=nR-I6d8j0P1JQy6dxIiZtG9Gz0DkfJFpk6EPCCpfnm8,3572
|
|
19
|
+
afwf_example-0.1.1.dist-info/licenses/AUTHORS.rst,sha256=oo38V9AD_y57Ac69mKmiItWquV29SRi2aTCdKQo531A,767
|
|
20
|
+
afwf_example-0.1.1.dist-info/licenses/LICENSE.txt,sha256=Fx_tQAmqkcvXayTPbicXlPPrnLKRf1JczCthjVuMS-M,1084
|
|
21
|
+
afwf_example-0.1.1.dist-info/METADATA,sha256=SVmlEnYmaiaxLxY6Xzy6UCHJU2l1XoMgFQGSKuvTJUI,5035
|
|
22
|
+
afwf_example-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
23
|
+
afwf_example-0.1.1.dist-info/entry_points.txt,sha256=Qatph6CdIhqH_OgAnF2oPvoPqQSybe9J9ankukEea8E,55
|
|
24
|
+
afwf_example-0.1.1.dist-info/top_level.txt,sha256=w9I2FP8QqZ-2tA2J-84SFqXioZvNxBK4xMpdpKR2S7o,13
|
|
25
|
+
afwf_example-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.. _about_author:
|
|
2
|
+
|
|
3
|
+
About the Author
|
|
4
|
+
------------------------------------------------------------------------------
|
|
5
|
+
::
|
|
6
|
+
|
|
7
|
+
(\ (\
|
|
8
|
+
( -.-)o
|
|
9
|
+
o_(")(")
|
|
10
|
+
|
|
11
|
+
**Sanhe Hu** is a seasoned software engineer with a deep passion for Python development since 2010. As an author and maintainer of `150+ open-source Python projects <https://pypi.org/user/machugwu/>`_, with over `15 million monthly downloads <https://github.com/MacHu-GWU>`_, I bring a wealth of experience to the table. As a Senior Solution Architect and Subject Matter Expert in AI, Data, Amazon Web Services, Cloud Engineering, DevOps, I thrive on helping clients with platform design, enterprise architecture, and strategic roadmaps.
|
|
12
|
+
|
|
13
|
+
Talk is cheap, show me the code:
|
|
14
|
+
|
|
15
|
+
- My Github: https://github.com/MacHu-GWU
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sanhe Hu <husanhe@email.com>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
afwf_example
|