afwf_github 1.0.1__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.
- afwf_github-1.0.1/AUTHORS.rst +15 -0
- afwf_github-1.0.1/LICENSE.txt +21 -0
- afwf_github-1.0.1/PKG-INFO +154 -0
- afwf_github-1.0.1/README.rst +95 -0
- afwf_github-1.0.1/afwf_github/__init__.py +1 -0
- afwf_github-1.0.1/afwf_github/api.py +2 -0
- afwf_github-1.0.1/afwf_github/cache.py +22 -0
- afwf_github-1.0.1/afwf_github/cli.py +295 -0
- afwf_github-1.0.1/afwf_github/config.py +44 -0
- afwf_github-1.0.1/afwf_github/constants.py +36 -0
- afwf_github-1.0.1/afwf_github/dataset.py +101 -0
- afwf_github-1.0.1/afwf_github/github.py +179 -0
- afwf_github-1.0.1/afwf_github/paths.py +93 -0
- afwf_github-1.0.1/afwf_github/type_hint.py +5 -0
- afwf_github-1.0.1/afwf_github/vendor/__init__.py +2 -0
- afwf_github-1.0.1/afwf_github/vendor/pytest_cov_helper.py +148 -0
- afwf_github-1.0.1/afwf_github.egg-info/PKG-INFO +154 -0
- afwf_github-1.0.1/afwf_github.egg-info/SOURCES.txt +27 -0
- afwf_github-1.0.1/afwf_github.egg-info/dependency_links.txt +1 -0
- afwf_github-1.0.1/afwf_github.egg-info/entry_points.txt +2 -0
- afwf_github-1.0.1/afwf_github.egg-info/requires.txt +34 -0
- afwf_github-1.0.1/afwf_github.egg-info/top_level.txt +1 -0
- afwf_github-1.0.1/pyproject.toml +134 -0
- afwf_github-1.0.1/setup.cfg +4 -0
- afwf_github-1.0.1/tests/test_api.py +17 -0
- afwf_github-1.0.1/tests/test_cli.py +132 -0
- afwf_github-1.0.1/tests/test_config.py +36 -0
- afwf_github-1.0.1/tests/test_dataset.py +40 -0
- afwf_github-1.0.1/tests/test_github.py +76 -0
|
@@ -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,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: afwf_github
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Alfred GitHub Workflow.
|
|
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_github-project
|
|
9
|
+
Project-URL: Documentation, https://afwf-github.readthedocs.io/en/latest/
|
|
10
|
+
Project-URL: Repository, https://github.com/MacHu-GWU/afwf_github-project
|
|
11
|
+
Project-URL: Issues, https://github.com/MacHu-GWU/afwf_github-project/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/MacHu-GWU/afwf_github-project/blob/main/release-history.rst
|
|
13
|
+
Project-URL: Download, https://pypi.org/pypi/afwf-github#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: pydantic<3.0.0,>=2.11.10
|
|
31
|
+
Requires-Dist: afwf<2.0.0,>=1.0.2
|
|
32
|
+
Requires-Dist: diskcache<6.0.0,>=5.6.3
|
|
33
|
+
Requires-Dist: PyGithub<3.0.0,>=2.8.1
|
|
34
|
+
Requires-Dist: git-web-url<2.0.0,>=1.0.2
|
|
35
|
+
Requires-Dist: sayt2<1.0.0,>=0.1.2
|
|
36
|
+
Requires-Dist: home-secret-toml<1.0.0,>=0.2.1
|
|
37
|
+
Requires-Dist: fire<1.0.0,>=0.6.0
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: rich<15.0.0,>=13.8.1; extra == "dev"
|
|
40
|
+
Provides-Extra: test
|
|
41
|
+
Requires-Dist: pytest<9.0.0,>=8.2.2; extra == "test"
|
|
42
|
+
Requires-Dist: pytest-cov<7.0.0,>=6.0.0; extra == "test"
|
|
43
|
+
Provides-Extra: doc
|
|
44
|
+
Requires-Dist: Sphinx<8.0.0,>=7.4.7; extra == "doc"
|
|
45
|
+
Requires-Dist: sphinx-copybutton<1.0.0,>=0.5.2; extra == "doc"
|
|
46
|
+
Requires-Dist: sphinx-design<1.0.0,>=0.6.1; extra == "doc"
|
|
47
|
+
Requires-Dist: sphinx-jinja<3.0.0,>=2.0.2; extra == "doc"
|
|
48
|
+
Requires-Dist: furo==2024.8.6; extra == "doc"
|
|
49
|
+
Requires-Dist: pygments<3.0.0,>=2.18.0; extra == "doc"
|
|
50
|
+
Requires-Dist: ipython<8.19.0,>=8.18.1; extra == "doc"
|
|
51
|
+
Requires-Dist: nbsphinx<1.0.0,>=0.8.12; extra == "doc"
|
|
52
|
+
Requires-Dist: rstobj==2.0.0; extra == "doc"
|
|
53
|
+
Requires-Dist: docfly==3.0.3; extra == "doc"
|
|
54
|
+
Provides-Extra: mise
|
|
55
|
+
Requires-Dist: PyGithub<3.0.0,>=2.8.0; extra == "mise"
|
|
56
|
+
Requires-Dist: httpx<1.0.0,>=0.28.0; extra == "mise"
|
|
57
|
+
Requires-Dist: tomli<3.0.0,>=2.0.0; python_version < "3.11" and extra == "mise"
|
|
58
|
+
Dynamic: license-file
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
.. image:: https://readthedocs.org/projects/afwf-github/badge/?version=latest
|
|
62
|
+
:target: https://afwf-github.readthedocs.io/en/latest/
|
|
63
|
+
:alt: Documentation Status
|
|
64
|
+
|
|
65
|
+
.. image:: https://github.com/MacHu-GWU/afwf_github-project/actions/workflows/main.yml/badge.svg
|
|
66
|
+
:target: https://github.com/MacHu-GWU/afwf_github-project/actions?query=workflow:CI
|
|
67
|
+
|
|
68
|
+
.. image:: https://codecov.io/gh/MacHu-GWU/afwf_github-project/branch/main/graph/badge.svg
|
|
69
|
+
:target: https://codecov.io/gh/MacHu-GWU/afwf_github-project
|
|
70
|
+
|
|
71
|
+
.. image:: https://img.shields.io/pypi/v/afwf-github.svg
|
|
72
|
+
:target: https://pypi.python.org/pypi/afwf-github
|
|
73
|
+
|
|
74
|
+
.. image:: https://img.shields.io/pypi/l/afwf-github.svg
|
|
75
|
+
:target: https://pypi.python.org/pypi/afwf-github
|
|
76
|
+
|
|
77
|
+
.. image:: https://img.shields.io/pypi/pyversions/afwf-github.svg
|
|
78
|
+
:target: https://pypi.python.org/pypi/afwf-github
|
|
79
|
+
|
|
80
|
+
.. image:: https://img.shields.io/badge/✍️_Release_History!--None.svg?style=social&logo=github
|
|
81
|
+
:target: https://github.com/MacHu-GWU/afwf_github-project/blob/main/release-history.rst
|
|
82
|
+
|
|
83
|
+
.. image:: https://img.shields.io/badge/⭐_Star_me_on_GitHub!--None.svg?style=social&logo=github
|
|
84
|
+
:target: https://github.com/MacHu-GWU/afwf_github-project
|
|
85
|
+
|
|
86
|
+
------
|
|
87
|
+
|
|
88
|
+
.. image:: https://img.shields.io/badge/Link-API-blue.svg
|
|
89
|
+
:target: https://afwf-github.readthedocs.io/en/latest/py-modindex.html
|
|
90
|
+
|
|
91
|
+
.. image:: https://img.shields.io/badge/Link-Install-blue.svg
|
|
92
|
+
:target: `install`_
|
|
93
|
+
|
|
94
|
+
.. image:: https://img.shields.io/badge/Link-GitHub-blue.svg
|
|
95
|
+
:target: https://github.com/MacHu-GWU/afwf_github-project
|
|
96
|
+
|
|
97
|
+
.. image:: https://img.shields.io/badge/Link-Submit_Issue-blue.svg
|
|
98
|
+
:target: https://github.com/MacHu-GWU/afwf_github-project/issues
|
|
99
|
+
|
|
100
|
+
.. image:: https://img.shields.io/badge/Link-Request_Feature-blue.svg
|
|
101
|
+
:target: https://github.com/MacHu-GWU/afwf_github-project/issues
|
|
102
|
+
|
|
103
|
+
.. image:: https://img.shields.io/badge/Link-Download-blue.svg
|
|
104
|
+
:target: https://pypi.org/pypi/afwf-github#files
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
Welcome to ``afwf_github`` Documentation
|
|
108
|
+
==============================================================================
|
|
109
|
+
.. image:: https://afwf-github.readthedocs.io/en/latest/_static/afwf_github-logo.png
|
|
110
|
+
:target: https://afwf-github.readthedocs.io/en/latest/
|
|
111
|
+
|
|
112
|
+
It is an `Alfred Workflow <https://www.alfredapp.com/workflows/>`_ for GitHub operations. There already is a PHP `alfred-github-workflow <https://github.com/gharlan/alfred-github-workflow>`_ library for this. But the searching is based on Alfred built-in word level filtering, which doesn't allow any typo, fuzzy, and full text search. This project aims to provide the best searching experience powered by `tantivy <https://github.com/quickwit-oss/tantivy>`_ (via `sayt2 <https://github.com/MacHu-GWU/sayt2-project>`_), a Rust-based full-text search engine.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
Install
|
|
116
|
+
------------------------------------------------------------------------------
|
|
117
|
+
1. Make sure you have `Alfred 5 + <https://www.alfredapp.com/>`_ installed and bought the `Power Pack <https://www.alfredapp.com/shop/>`_.
|
|
118
|
+
2. Go to `Release <https://github.com/MacHu-GWU/afwf_github-project/releases>`_, download the latest release.
|
|
119
|
+
3. Double click the file to install.
|
|
120
|
+
4. Prepare your GitHub Personal Access Token: go to https://github.com/settings/tokens, create a new token, make sure you checked ``repo -> public_repo``, ``admin:org -> read:org``, ``admin:enterprise -> read:enterprise`` so the workflow can get your public repo name and url information. If you want to get your private repo as well, you should check ``repo (Full control of private repositories)``.
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
Usage
|
|
124
|
+
------------------------------------------------------------------------------
|
|
125
|
+
1. Configuration.
|
|
126
|
+
|
|
127
|
+
In Alfred UI, type ``gh-config``, it should open the ~/.alfred-afwf/afwf_github/config.json``
|
|
128
|
+
|
|
129
|
+
.. image:: https://github.com/MacHu-GWU/afwf_github-project/assets/6800411/2acff3ad-8a90-4326-8f64-3a54df2da11f
|
|
130
|
+
|
|
131
|
+
2. Build Index
|
|
132
|
+
|
|
133
|
+
In Alfred UI, type ``gh-rebuild-index``, it should start to crawl your GitHub repos. It will take a while to finish. You can check the progress in the ``~/.alfred-afwf/afwf_github/.repo_index/``
|
|
134
|
+
|
|
135
|
+
.. image:: https://github.com/MacHu-GWU/afwf_github-project/assets/6800411/59ce941d-a22a-4fb5-8013-c6a14ec5ca56
|
|
136
|
+
|
|
137
|
+
3. Search GitHub
|
|
138
|
+
|
|
139
|
+
In Alfred UI, type ``gh ${query}``, it should show the following UI:
|
|
140
|
+
|
|
141
|
+
.. image:: https://github.com/MacHu-GWU/afwf_github-project/assets/6800411/57ea7aa5-d2e0-4b73-8e66-632453418d92
|
|
142
|
+
|
|
143
|
+
4. Open Git Repo in Browser
|
|
144
|
+
|
|
145
|
+
Copy any absolute path of a file in any git repo, type ``gh-view-in-browser ${path}`` then hit ``Enter``, it should open the repo in browser.
|
|
146
|
+
|
|
147
|
+
.. image:: https://github.com/MacHu-GWU/afwf_github-project/assets/6800411/e863fac8-e9b0-4301-93c0-d745059e4346
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
Trouble Shooting
|
|
151
|
+
------------------------------------------------------------------------------
|
|
152
|
+
1. ``gh ${query}`` doesn't show any result.
|
|
153
|
+
|
|
154
|
+
Check the ``${HOME}/.alfred-afwf/afwf_github/`` folder, if there's no folder name equal to your github username(where to store the index), it means the Workflow failed to crawl your GitHub repos. Please double check ``${HOME}/.alfred-afwf/afwf_github/config.json`` to make sure you have the correct GitHub Personal Access Token.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
|
|
2
|
+
.. image:: https://readthedocs.org/projects/afwf-github/badge/?version=latest
|
|
3
|
+
:target: https://afwf-github.readthedocs.io/en/latest/
|
|
4
|
+
:alt: Documentation Status
|
|
5
|
+
|
|
6
|
+
.. image:: https://github.com/MacHu-GWU/afwf_github-project/actions/workflows/main.yml/badge.svg
|
|
7
|
+
:target: https://github.com/MacHu-GWU/afwf_github-project/actions?query=workflow:CI
|
|
8
|
+
|
|
9
|
+
.. image:: https://codecov.io/gh/MacHu-GWU/afwf_github-project/branch/main/graph/badge.svg
|
|
10
|
+
:target: https://codecov.io/gh/MacHu-GWU/afwf_github-project
|
|
11
|
+
|
|
12
|
+
.. image:: https://img.shields.io/pypi/v/afwf-github.svg
|
|
13
|
+
:target: https://pypi.python.org/pypi/afwf-github
|
|
14
|
+
|
|
15
|
+
.. image:: https://img.shields.io/pypi/l/afwf-github.svg
|
|
16
|
+
:target: https://pypi.python.org/pypi/afwf-github
|
|
17
|
+
|
|
18
|
+
.. image:: https://img.shields.io/pypi/pyversions/afwf-github.svg
|
|
19
|
+
:target: https://pypi.python.org/pypi/afwf-github
|
|
20
|
+
|
|
21
|
+
.. image:: https://img.shields.io/badge/✍️_Release_History!--None.svg?style=social&logo=github
|
|
22
|
+
:target: https://github.com/MacHu-GWU/afwf_github-project/blob/main/release-history.rst
|
|
23
|
+
|
|
24
|
+
.. image:: https://img.shields.io/badge/⭐_Star_me_on_GitHub!--None.svg?style=social&logo=github
|
|
25
|
+
:target: https://github.com/MacHu-GWU/afwf_github-project
|
|
26
|
+
|
|
27
|
+
------
|
|
28
|
+
|
|
29
|
+
.. image:: https://img.shields.io/badge/Link-API-blue.svg
|
|
30
|
+
:target: https://afwf-github.readthedocs.io/en/latest/py-modindex.html
|
|
31
|
+
|
|
32
|
+
.. image:: https://img.shields.io/badge/Link-Install-blue.svg
|
|
33
|
+
:target: `install`_
|
|
34
|
+
|
|
35
|
+
.. image:: https://img.shields.io/badge/Link-GitHub-blue.svg
|
|
36
|
+
:target: https://github.com/MacHu-GWU/afwf_github-project
|
|
37
|
+
|
|
38
|
+
.. image:: https://img.shields.io/badge/Link-Submit_Issue-blue.svg
|
|
39
|
+
:target: https://github.com/MacHu-GWU/afwf_github-project/issues
|
|
40
|
+
|
|
41
|
+
.. image:: https://img.shields.io/badge/Link-Request_Feature-blue.svg
|
|
42
|
+
:target: https://github.com/MacHu-GWU/afwf_github-project/issues
|
|
43
|
+
|
|
44
|
+
.. image:: https://img.shields.io/badge/Link-Download-blue.svg
|
|
45
|
+
:target: https://pypi.org/pypi/afwf-github#files
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
Welcome to ``afwf_github`` Documentation
|
|
49
|
+
==============================================================================
|
|
50
|
+
.. image:: https://afwf-github.readthedocs.io/en/latest/_static/afwf_github-logo.png
|
|
51
|
+
:target: https://afwf-github.readthedocs.io/en/latest/
|
|
52
|
+
|
|
53
|
+
It is an `Alfred Workflow <https://www.alfredapp.com/workflows/>`_ for GitHub operations. There already is a PHP `alfred-github-workflow <https://github.com/gharlan/alfred-github-workflow>`_ library for this. But the searching is based on Alfred built-in word level filtering, which doesn't allow any typo, fuzzy, and full text search. This project aims to provide the best searching experience powered by `tantivy <https://github.com/quickwit-oss/tantivy>`_ (via `sayt2 <https://github.com/MacHu-GWU/sayt2-project>`_), a Rust-based full-text search engine.
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
Install
|
|
57
|
+
------------------------------------------------------------------------------
|
|
58
|
+
1. Make sure you have `Alfred 5 + <https://www.alfredapp.com/>`_ installed and bought the `Power Pack <https://www.alfredapp.com/shop/>`_.
|
|
59
|
+
2. Go to `Release <https://github.com/MacHu-GWU/afwf_github-project/releases>`_, download the latest release.
|
|
60
|
+
3. Double click the file to install.
|
|
61
|
+
4. Prepare your GitHub Personal Access Token: go to https://github.com/settings/tokens, create a new token, make sure you checked ``repo -> public_repo``, ``admin:org -> read:org``, ``admin:enterprise -> read:enterprise`` so the workflow can get your public repo name and url information. If you want to get your private repo as well, you should check ``repo (Full control of private repositories)``.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
Usage
|
|
65
|
+
------------------------------------------------------------------------------
|
|
66
|
+
1. Configuration.
|
|
67
|
+
|
|
68
|
+
In Alfred UI, type ``gh-config``, it should open the ~/.alfred-afwf/afwf_github/config.json``
|
|
69
|
+
|
|
70
|
+
.. image:: https://github.com/MacHu-GWU/afwf_github-project/assets/6800411/2acff3ad-8a90-4326-8f64-3a54df2da11f
|
|
71
|
+
|
|
72
|
+
2. Build Index
|
|
73
|
+
|
|
74
|
+
In Alfred UI, type ``gh-rebuild-index``, it should start to crawl your GitHub repos. It will take a while to finish. You can check the progress in the ``~/.alfred-afwf/afwf_github/.repo_index/``
|
|
75
|
+
|
|
76
|
+
.. image:: https://github.com/MacHu-GWU/afwf_github-project/assets/6800411/59ce941d-a22a-4fb5-8013-c6a14ec5ca56
|
|
77
|
+
|
|
78
|
+
3. Search GitHub
|
|
79
|
+
|
|
80
|
+
In Alfred UI, type ``gh ${query}``, it should show the following UI:
|
|
81
|
+
|
|
82
|
+
.. image:: https://github.com/MacHu-GWU/afwf_github-project/assets/6800411/57ea7aa5-d2e0-4b73-8e66-632453418d92
|
|
83
|
+
|
|
84
|
+
4. Open Git Repo in Browser
|
|
85
|
+
|
|
86
|
+
Copy any absolute path of a file in any git repo, type ``gh-view-in-browser ${path}`` then hit ``Enter``, it should open the repo in browser.
|
|
87
|
+
|
|
88
|
+
.. image:: https://github.com/MacHu-GWU/afwf_github-project/assets/6800411/e863fac8-e9b0-4301-93c0-d745059e4346
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
Trouble Shooting
|
|
92
|
+
------------------------------------------------------------------------------
|
|
93
|
+
1. ``gh ${query}`` doesn't show any result.
|
|
94
|
+
|
|
95
|
+
Check the ``${HOME}/.alfred-afwf/afwf_github/`` folder, if there's no folder name equal to your github username(where to store the index), it means the Workflow failed to crawl your GitHub repos. Please double check ``${HOME}/.alfred-afwf/afwf_github/config.json`` to make sure you have the correct GitHub Personal Access Token.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Factory for diskcache instances used to store GitHub API responses.
|
|
5
|
+
|
|
6
|
+
Each GitHub user gets their own cache directory under their per-user data
|
|
7
|
+
directory. sayt2 datasets manage their own internal search result cache
|
|
8
|
+
separately — this module only covers GitHub API response caching.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from diskcache import Cache
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def make_cache(dir_cache: Path) -> Cache:
|
|
17
|
+
"""Create a ``diskcache.Cache`` at *dir_cache*.
|
|
18
|
+
|
|
19
|
+
diskcache creates the directory if it does not exist, so callers do not
|
|
20
|
+
need to ensure the path exists beforehand.
|
|
21
|
+
"""
|
|
22
|
+
return Cache(dir_cache)
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
import typing as T
|
|
6
|
+
import functools
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from functools import cached_property
|
|
9
|
+
|
|
10
|
+
import fire
|
|
11
|
+
import afwf.api as afwf
|
|
12
|
+
import git_web_url.api as gwu
|
|
13
|
+
from git_web_url.exc import NotGitRepoError
|
|
14
|
+
|
|
15
|
+
from .config import Config
|
|
16
|
+
from .paths import path_enum
|
|
17
|
+
from .dataset import create_repo_dataset
|
|
18
|
+
|
|
19
|
+
_CONFIG_TEMPLATE = {
|
|
20
|
+
"pac_token": None,
|
|
21
|
+
"pac_token_home_secret_toml_path": None,
|
|
22
|
+
"cache_expire": 2_592_000,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_log_error = afwf.log_error(
|
|
26
|
+
log_file=path_enum.path_error_log,
|
|
27
|
+
tb_limit=10,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _error_sf(exc: Exception) -> afwf.ScriptFilter:
|
|
32
|
+
item = afwf.Item(
|
|
33
|
+
title=f"{type(exc).__name__}: {exc}",
|
|
34
|
+
subtitle=f"Press Enter to open the error log: {path_enum.path_error_log}",
|
|
35
|
+
icon=afwf.Icon.from_image_file(path=afwf.IconFileEnum.error),
|
|
36
|
+
valid=True,
|
|
37
|
+
)
|
|
38
|
+
item.open_file(str(path_enum.path_error_log))
|
|
39
|
+
return afwf.ScriptFilter(items=[item])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _config_error_sf(config_path: Path) -> afwf.ScriptFilter:
|
|
43
|
+
item = afwf.Item(
|
|
44
|
+
title=f"Config file not found: {config_path}",
|
|
45
|
+
subtitle="Press Enter to open the setup guide on GitHub",
|
|
46
|
+
icon=afwf.Icon.from_image_file(path=afwf.IconFileEnum.error),
|
|
47
|
+
valid=True,
|
|
48
|
+
)
|
|
49
|
+
item.open_url("https://github.com/MacHu-GWU/afwf_github-project")
|
|
50
|
+
return afwf.ScriptFilter(items=[item])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def require_config(method: T.Callable) -> T.Callable:
|
|
54
|
+
"""Decorator that resolves and loads config before calling a CLI method.
|
|
55
|
+
|
|
56
|
+
Resolution order:
|
|
57
|
+
1. If ``self.config_file`` is set, load from that path.
|
|
58
|
+
2. Otherwise use the default path (``path_enum.path_config_json``).
|
|
59
|
+
|
|
60
|
+
On any failure (file missing, parse error) the decorator outputs a
|
|
61
|
+
single error ``Item`` that opens the project README on Enter, then
|
|
62
|
+
returns early so the wrapped method is never called.
|
|
63
|
+
|
|
64
|
+
On success, ``self._config`` is populated and the wrapped method runs
|
|
65
|
+
normally.
|
|
66
|
+
|
|
67
|
+
``functools.wraps`` is still required in Python 3.10+ to copy
|
|
68
|
+
``__name__``, ``__doc__``, ``__wrapped__``, etc. onto the wrapper —
|
|
69
|
+
``ParamSpec`` (added in 3.10) improves type-checker inference for
|
|
70
|
+
decorators but does not replace ``functools.wraps``.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
@functools.wraps(method)
|
|
74
|
+
def wrapper(self: "Command", *args, **kwargs):
|
|
75
|
+
if self.config_file is not None:
|
|
76
|
+
config_path = Path(self.config_file).expanduser().resolve()
|
|
77
|
+
else:
|
|
78
|
+
config_path = path_enum.path_config_json
|
|
79
|
+
|
|
80
|
+
if not config_path.exists():
|
|
81
|
+
_config_error_sf(config_path).send_feedback()
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
self._config = Config.load(config_path)
|
|
86
|
+
except Exception:
|
|
87
|
+
_config_error_sf(config_path).send_feedback()
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
return method(self, *args, **kwargs)
|
|
91
|
+
|
|
92
|
+
return wrapper
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class Command:
|
|
96
|
+
"""Alfred GitHub Workflow CLI.
|
|
97
|
+
|
|
98
|
+
All subcommands accept an optional ``--config-file`` argument (absolute
|
|
99
|
+
or relative path). When omitted, config is loaded from the default
|
|
100
|
+
location via ``default_config``.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, config_file: str | None = None):
|
|
104
|
+
self.config_file = config_file
|
|
105
|
+
self._config: Config | None = None
|
|
106
|
+
|
|
107
|
+
@cached_property
|
|
108
|
+
def default_config(self) -> Config:
|
|
109
|
+
"""Load config from the default path (``~/.alfred-afwf/afwf_github/config.json``)."""
|
|
110
|
+
return Config.load(path_enum.path_config_json)
|
|
111
|
+
|
|
112
|
+
@afwf.log_error(
|
|
113
|
+
|
|
114
|
+
)
|
|
115
|
+
def edit_config(self) -> None:
|
|
116
|
+
"""Script Filter: open config.json in the default editor.
|
|
117
|
+
|
|
118
|
+
Creates a blank template at the default path if the file does not yet
|
|
119
|
+
exist, then opens it via Alfred's Open File action.
|
|
120
|
+
|
|
121
|
+
Alfred Script field (dev):
|
|
122
|
+
.venv/bin/afwf-github edit-config
|
|
123
|
+
|
|
124
|
+
Alfred Script field (prod):
|
|
125
|
+
~/.local/bin/uvx --from afwf_github==<ver> afwf-github edit-config
|
|
126
|
+
"""
|
|
127
|
+
@_log_error
|
|
128
|
+
def _run():
|
|
129
|
+
config_path = path_enum.path_config_json
|
|
130
|
+
if not config_path.exists():
|
|
131
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
config_path.write_text(json.dumps(_CONFIG_TEMPLATE, indent=4))
|
|
133
|
+
item = afwf.Item(
|
|
134
|
+
title="Open and edit config.json",
|
|
135
|
+
subtitle=str(config_path),
|
|
136
|
+
icon=afwf.Icon.from_image_file(path=afwf.IconFileEnum.file),
|
|
137
|
+
)
|
|
138
|
+
item.open_file(str(config_path))
|
|
139
|
+
afwf.ScriptFilter(items=[item]).send_feedback()
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
_run()
|
|
143
|
+
except Exception as e:
|
|
144
|
+
_error_sf(e).send_feedback()
|
|
145
|
+
|
|
146
|
+
@require_config
|
|
147
|
+
def view_in_browser(self, path: str = "") -> None:
|
|
148
|
+
"""Script Filter: given a local file or directory path, open its GitHub URL in the browser.
|
|
149
|
+
|
|
150
|
+
Alfred Script field (dev):
|
|
151
|
+
.venv/bin/afwf-github view-in-browser --path '{query}'
|
|
152
|
+
|
|
153
|
+
Alfred Script field (prod):
|
|
154
|
+
~/.local/bin/uvx --from afwf_github==<ver> afwf-github view-in-browser --path '{query}'
|
|
155
|
+
"""
|
|
156
|
+
@_log_error
|
|
157
|
+
def _run():
|
|
158
|
+
if not path.strip():
|
|
159
|
+
afwf.ScriptFilter(
|
|
160
|
+
items=[
|
|
161
|
+
afwf.Item(
|
|
162
|
+
title="Type or paste the absolute path of a local file or directory"
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
).send_feedback()
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
url = gwu.get_web_url(Path(path))
|
|
170
|
+
item = afwf.Item(
|
|
171
|
+
title=f"Open in browser: {url}",
|
|
172
|
+
subtitle=f"Local path: {path}",
|
|
173
|
+
icon=afwf.Icon.from_image_file(path=afwf.IconFileEnum.internet),
|
|
174
|
+
)
|
|
175
|
+
item.open_url(url)
|
|
176
|
+
except NotGitRepoError:
|
|
177
|
+
item = afwf.Item(
|
|
178
|
+
title=f"Not a git repository path: {path}",
|
|
179
|
+
subtitle="Only paths inside a git repo with a remote can be opened in browser",
|
|
180
|
+
icon=afwf.Icon.from_image_file(path=afwf.IconFileEnum.error),
|
|
181
|
+
valid=False,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
afwf.ScriptFilter(items=[item]).send_feedback()
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
_run()
|
|
188
|
+
except Exception as e:
|
|
189
|
+
_error_sf(e).send_feedback()
|
|
190
|
+
|
|
191
|
+
@require_config
|
|
192
|
+
def search_repo(self, query: str = "") -> None:
|
|
193
|
+
"""Script Filter: search GitHub repositories in the local index.
|
|
194
|
+
|
|
195
|
+
Alfred Script field (dev):
|
|
196
|
+
.venv/bin/afwf-github search-repo --query '{query}'
|
|
197
|
+
|
|
198
|
+
Alfred Script field (prod):
|
|
199
|
+
~/.local/bin/uvx --from afwf_github==<ver> afwf-github search-repo --query '{query}'
|
|
200
|
+
"""
|
|
201
|
+
@_log_error
|
|
202
|
+
def _run():
|
|
203
|
+
if not query.strip():
|
|
204
|
+
afwf.ScriptFilter(
|
|
205
|
+
items=[afwf.Item(title="Type to search GitHub repositories ...")]
|
|
206
|
+
).send_feedback()
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
dataset = create_repo_dataset(config=self._config)
|
|
210
|
+
result = dataset.search(query=query, limit=50)
|
|
211
|
+
|
|
212
|
+
if not result.hits:
|
|
213
|
+
afwf.ScriptFilter(
|
|
214
|
+
items=[
|
|
215
|
+
afwf.Item(
|
|
216
|
+
title=f"No repository found for: {query!r}",
|
|
217
|
+
icon=afwf.Icon.from_image_file(path=afwf.IconFileEnum.error),
|
|
218
|
+
valid=False,
|
|
219
|
+
)
|
|
220
|
+
]
|
|
221
|
+
).send_feedback()
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
items = []
|
|
225
|
+
for hit in result.hits:
|
|
226
|
+
repo = hit.source
|
|
227
|
+
account_name = repo["acc"]
|
|
228
|
+
repo_name = repo["repo"]
|
|
229
|
+
repo_description = repo.get("desc", "No description")
|
|
230
|
+
url = f"https://github.com/{account_name}/{repo_name}"
|
|
231
|
+
item = afwf.Item(
|
|
232
|
+
title=f"{account_name}/{repo_name}",
|
|
233
|
+
subtitle=repo_description,
|
|
234
|
+
autocomplete=f"{account_name}/{repo_name}",
|
|
235
|
+
)
|
|
236
|
+
item.open_url(url)
|
|
237
|
+
items.append(item)
|
|
238
|
+
afwf.ScriptFilter(items=items).send_feedback()
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
_run()
|
|
242
|
+
except Exception as e:
|
|
243
|
+
_error_sf(e).send_feedback()
|
|
244
|
+
|
|
245
|
+
@require_config
|
|
246
|
+
def rebuild_index(self) -> None:
|
|
247
|
+
"""Script Filter: show a single item that triggers ``rebuild-index-action`` on Enter.
|
|
248
|
+
|
|
249
|
+
Alfred Script field (dev):
|
|
250
|
+
.venv/bin/afwf-github rebuild-index
|
|
251
|
+
|
|
252
|
+
Alfred Script field (prod):
|
|
253
|
+
~/.local/bin/uvx --from afwf_github==<ver> afwf-github rebuild-index
|
|
254
|
+
"""
|
|
255
|
+
@_log_error
|
|
256
|
+
def _run():
|
|
257
|
+
bin_cli = Path(sys.executable).parent / "afwf-github"
|
|
258
|
+
cmd = f"{bin_cli} rebuild-index-action"
|
|
259
|
+
if self.config_file is not None:
|
|
260
|
+
cmd += f" --config-file {self.config_file!r}"
|
|
261
|
+
|
|
262
|
+
item = afwf.Item(
|
|
263
|
+
title="Rebuild Index for GitHub Alfred Workflow",
|
|
264
|
+
subtitle="Hit Enter to rebuild — may take 10–20 seconds",
|
|
265
|
+
icon=afwf.Icon.from_image_file(path=afwf.IconFileEnum.reset),
|
|
266
|
+
)
|
|
267
|
+
item.run_script(cmd)
|
|
268
|
+
afwf.ScriptFilter(items=[item]).send_feedback()
|
|
269
|
+
|
|
270
|
+
try:
|
|
271
|
+
_run()
|
|
272
|
+
except Exception as e:
|
|
273
|
+
_error_sf(e).send_feedback()
|
|
274
|
+
|
|
275
|
+
@require_config
|
|
276
|
+
@_log_error
|
|
277
|
+
def rebuild_index_action(self) -> None:
|
|
278
|
+
"""Rebuild the local repo search index by re-fetching data from GitHub.
|
|
279
|
+
|
|
280
|
+
Called by Alfred's Run Script widget — NOT a Script Filter.
|
|
281
|
+
|
|
282
|
+
Alfred Run Script field (dev):
|
|
283
|
+
.venv/bin/afwf-github rebuild-index-action
|
|
284
|
+
|
|
285
|
+
Alfred Run Script field (prod):
|
|
286
|
+
~/.local/bin/uvx --from afwf_github==<ver> afwf-github rebuild-index-action
|
|
287
|
+
"""
|
|
288
|
+
create_repo_dataset(config=self._config).search(
|
|
289
|
+
query="",
|
|
290
|
+
refresh=True,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def run():
|
|
295
|
+
fire.Fire(Command)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from functools import cached_property
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, model_validator
|
|
8
|
+
from github import Github, Auth
|
|
9
|
+
from home_secret_toml.api import hs
|
|
10
|
+
|
|
11
|
+
from .paths import path_enum
|
|
12
|
+
|
|
13
|
+
path_config_json = path_enum.path_config_json
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Config(BaseModel):
|
|
17
|
+
model_config = ConfigDict(extra="forbid")
|
|
18
|
+
|
|
19
|
+
pac_token: str | None = None
|
|
20
|
+
pac_token_home_secret_toml_path: str | None = None
|
|
21
|
+
cache_expire: int = 30 * 24 * 3600
|
|
22
|
+
|
|
23
|
+
@model_validator(mode="after")
|
|
24
|
+
def check_pac_token(self):
|
|
25
|
+
if self.pac_token is None and self.pac_token_home_secret_toml_path is None:
|
|
26
|
+
raise ValueError("Must provide pac_token or pac_token_home_secret_toml_path")
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def load(cls, path: Path) -> "Config": # pragma: no cover
|
|
31
|
+
return cls.model_validate(json.loads(path.read_text()))
|
|
32
|
+
|
|
33
|
+
def dump(self, path: Path): # pragma: no cover
|
|
34
|
+
path.write_text(self.model_dump_json(indent=4))
|
|
35
|
+
|
|
36
|
+
@cached_property
|
|
37
|
+
def gh(self):
|
|
38
|
+
if self.pac_token is not None:
|
|
39
|
+
pac_token = self.pac_token
|
|
40
|
+
elif self.pac_token_home_secret_toml_path is not None:
|
|
41
|
+
pac_token = hs.v(self.pac_token_home_secret_toml_path)
|
|
42
|
+
else:
|
|
43
|
+
raise ValueError("Must provide pac_token or pac_token_home_secret_toml")
|
|
44
|
+
return Github(auth=Auth.Token(pac_token))
|