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.
@@ -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,2 @@
1
+ # -*- coding: utf-8 -*-
2
+
@@ -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))