phab-feedback 0.1.0__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.
- phab_feedback-0.1.0/LICENSE +21 -0
- phab_feedback-0.1.0/PKG-INFO +184 -0
- phab_feedback-0.1.0/README.md +155 -0
- phab_feedback-0.1.0/pyproject.toml +41 -0
- phab_feedback-0.1.0/setup.cfg +4 -0
- phab_feedback-0.1.0/src/phab_feedback/__init__.py +3 -0
- phab_feedback-0.1.0/src/phab_feedback/__main__.py +3 -0
- phab_feedback-0.1.0/src/phab_feedback/api.py +107 -0
- phab_feedback-0.1.0/src/phab_feedback/cli.py +241 -0
- phab_feedback-0.1.0/src/phab_feedback/config.py +276 -0
- phab_feedback-0.1.0/src/phab_feedback/errors.py +21 -0
- phab_feedback-0.1.0/src/phab_feedback/py.typed +0 -0
- phab_feedback-0.1.0/src/phab_feedback/service.py +407 -0
- phab_feedback-0.1.0/src/phab_feedback/transport.py +62 -0
- phab_feedback-0.1.0/src/phab_feedback.egg-info/PKG-INFO +184 -0
- phab_feedback-0.1.0/src/phab_feedback.egg-info/SOURCES.txt +20 -0
- phab_feedback-0.1.0/src/phab_feedback.egg-info/dependency_links.txt +1 -0
- phab_feedback-0.1.0/src/phab_feedback.egg-info/entry_points.txt +2 -0
- phab_feedback-0.1.0/src/phab_feedback.egg-info/top_level.txt +1 -0
- phab_feedback-0.1.0/tests/test_cli.py +80 -0
- phab_feedback-0.1.0/tests/test_config.py +178 -0
- phab_feedback-0.1.0/tests/test_service.py +237 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Logan Rosen
|
|
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,184 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phab-feedback
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for Phabricator and Phorge review feedback workflows
|
|
5
|
+
Author: Logan Rosen
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/loganrosen/phab-feedback
|
|
8
|
+
Project-URL: Repository, https://github.com/loganrosen/phab-feedback
|
|
9
|
+
Project-URL: Issues, https://github.com/loganrosen/phab-feedback/issues
|
|
10
|
+
Keywords: phabricator,phorge,code-review,cli
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Software Development :: Version Control
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# phab-feedback
|
|
31
|
+
|
|
32
|
+
`phab-feedback` is a small command-line client for reviewing and acting on
|
|
33
|
+
Phabricator and Phorge feedback. It keeps inline replies as real inline-thread
|
|
34
|
+
replies, exposes draft actions explicitly, and produces structured JSON suitable
|
|
35
|
+
for both people and automation.
|
|
36
|
+
|
|
37
|
+
## How this differs
|
|
38
|
+
|
|
39
|
+
`arc` and `moz-phab` handle author-side Differential workflows such as creating
|
|
40
|
+
or updating revisions from local commits, with `arc` also providing landing
|
|
41
|
+
workflows. `phabfive` provides broader Conduit-based access to Phabricator and
|
|
42
|
+
Phorge applications such as Maniphest, Paste, Diffusion, Passphrase, and User.
|
|
43
|
+
|
|
44
|
+
`phab-feedback` complements those tools by focusing on structured Differential
|
|
45
|
+
feedback, inline threads, explicit draft actions, and browser-only mutations
|
|
46
|
+
that Conduit does not expose. Its optional Mozilla Review Helper rating and
|
|
47
|
+
AI-review commands remain isolated from the generic Phabricator and Phorge
|
|
48
|
+
behavior.
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
Python 3.10 or newer is required.
|
|
53
|
+
|
|
54
|
+
After the first PyPI release is published, run the CLI without installing it:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uvx phab-feedback --help
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For a persistent installation, `uv` is recommended:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv tool install phab-feedback
|
|
64
|
+
phab-feedback --help
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`pipx` is an alternative:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pipx install phab-feedback
|
|
71
|
+
phab-feedback --help
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
To try unreleased development from GitHub:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv tool install git+https://github.com/loganrosen/phab-feedback.git
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
For local source development:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git clone https://github.com/loganrosen/phab-feedback.git
|
|
84
|
+
cd phab-feedback
|
|
85
|
+
python3 -m pip install -e .
|
|
86
|
+
python3 -m unittest discover -s tests
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Configuration
|
|
90
|
+
|
|
91
|
+
Choose a host with `--host`, `PHAB_FEEDBACK_HOST`, or
|
|
92
|
+
`~/.config/phab-feedback/config.json`, in that order:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"host": "https://phabricator.example.com",
|
|
97
|
+
"cookie_name": "phsid"
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
If no host is configured and `~/.arcrc` contains exactly one host,
|
|
102
|
+
`phab-feedback` uses it. Conduit tokens come from `PHAB_FEEDBACK_TOKEN` or the
|
|
103
|
+
matching `~/.arcrc` entry. Tokens are never accepted as command-line arguments.
|
|
104
|
+
|
|
105
|
+
Internal web actions need a logged-in browser session. Set
|
|
106
|
+
`PHAB_FEEDBACK_SESSION_COOKIE` to a complete `Cookie` header value, or to the
|
|
107
|
+
value of the configured session cookie. The value is never printed. Mozilla
|
|
108
|
+
Phabricator users can instead pass `--firefox-cookies` to discover the session
|
|
109
|
+
from a local Firefox profile; `--firefox-profile` selects a specific profile.
|
|
110
|
+
|
|
111
|
+
`XDG_CONFIG_HOME` and `PHAB_FEEDBACK_ARCRC` are respected. The config file is
|
|
112
|
+
for non-secret settings; keep tokens in `.arcrc` or the environment and session
|
|
113
|
+
cookies in the environment or browser store.
|
|
114
|
+
|
|
115
|
+
## Commands
|
|
116
|
+
|
|
117
|
+
All successful commands write JSON to stdout. Message-taking commands accept
|
|
118
|
+
exactly one of `--message`, `--message-file PATH`, or `--message-file -`.
|
|
119
|
+
When stdin is redirected, it is also accepted without an option.
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Read the complete chronological feedback timeline.
|
|
123
|
+
phab-feedback timeline D123
|
|
124
|
+
|
|
125
|
+
# Post an immediate top-level revision comment through Conduit.
|
|
126
|
+
phab-feedback comment D123 --message-file reply.txt
|
|
127
|
+
|
|
128
|
+
# Create a true inline-thread reply draft, then publish it separately.
|
|
129
|
+
printf '%s\n' 'Handled in the latest update.' |
|
|
130
|
+
phab-feedback reply-inline D123 456
|
|
131
|
+
phab-feedback submit D123
|
|
132
|
+
|
|
133
|
+
# Explicitly create and publish an inline reply in one invocation.
|
|
134
|
+
phab-feedback reply-inline D123 456 --message 'Done.' --submit
|
|
135
|
+
|
|
136
|
+
# Remove an accidental top-level comment after validating its type.
|
|
137
|
+
phab-feedback remove-comment D123 789
|
|
138
|
+
|
|
139
|
+
# Create Done drafts, then submit them.
|
|
140
|
+
phab-feedback mark-done D123 456 457
|
|
141
|
+
phab-feedback submit D123
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`timeline`, `comment`, and the metadata validation used by mutations rely on
|
|
145
|
+
standard Conduit APIs. Inline reply drafting, top-level comment removal, Done
|
|
146
|
+
drafting, and draft submission use internal web endpoints available in upstream
|
|
147
|
+
Phabricator and Phorge. Those endpoints are less stable than Conduit and may
|
|
148
|
+
change between server releases.
|
|
149
|
+
|
|
150
|
+
These commands are **Mozilla-only** because they use the Review Helper
|
|
151
|
+
extension, not upstream Phabricator:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
phab-feedback mark-helpful D123 456
|
|
155
|
+
phab-feedback mark-unhelpful D123 457
|
|
156
|
+
phab-feedback request-ai-review D123
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Helpful and unhelpful ratings take effect immediately. Inline replies and Done
|
|
160
|
+
states remain drafts until `submit`; `reply-inline --submit` is the only
|
|
161
|
+
intentional combined workflow. Rating, Done, and reply actions are never
|
|
162
|
+
combined implicitly.
|
|
163
|
+
|
|
164
|
+
The repository also includes an optional thin agent skill. It contains workflow
|
|
165
|
+
and approval guidance only; the CLI remains the single implementation of all
|
|
166
|
+
deterministic behavior. Install it through the open Skills CLI:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npx skills add loganrosen/phab-feedback@phab-feedback -g
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The Skills CLI handles the supported agent-specific installation paths. The
|
|
173
|
+
Python package does not modify agent configuration or install the skill
|
|
174
|
+
automatically.
|
|
175
|
+
|
|
176
|
+
## Security
|
|
177
|
+
|
|
178
|
+
Credentials are sent only in request headers or bodies to the configured host.
|
|
179
|
+
Errors omit request bodies, tokens, and cookies. Avoid enabling shell tracing
|
|
180
|
+
while setting credential environment variables.
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# phab-feedback
|
|
2
|
+
|
|
3
|
+
`phab-feedback` is a small command-line client for reviewing and acting on
|
|
4
|
+
Phabricator and Phorge feedback. It keeps inline replies as real inline-thread
|
|
5
|
+
replies, exposes draft actions explicitly, and produces structured JSON suitable
|
|
6
|
+
for both people and automation.
|
|
7
|
+
|
|
8
|
+
## How this differs
|
|
9
|
+
|
|
10
|
+
`arc` and `moz-phab` handle author-side Differential workflows such as creating
|
|
11
|
+
or updating revisions from local commits, with `arc` also providing landing
|
|
12
|
+
workflows. `phabfive` provides broader Conduit-based access to Phabricator and
|
|
13
|
+
Phorge applications such as Maniphest, Paste, Diffusion, Passphrase, and User.
|
|
14
|
+
|
|
15
|
+
`phab-feedback` complements those tools by focusing on structured Differential
|
|
16
|
+
feedback, inline threads, explicit draft actions, and browser-only mutations
|
|
17
|
+
that Conduit does not expose. Its optional Mozilla Review Helper rating and
|
|
18
|
+
AI-review commands remain isolated from the generic Phabricator and Phorge
|
|
19
|
+
behavior.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
Python 3.10 or newer is required.
|
|
24
|
+
|
|
25
|
+
After the first PyPI release is published, run the CLI without installing it:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uvx phab-feedback --help
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
For a persistent installation, `uv` is recommended:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv tool install phab-feedback
|
|
35
|
+
phab-feedback --help
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`pipx` is an alternative:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pipx install phab-feedback
|
|
42
|
+
phab-feedback --help
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
To try unreleased development from GitHub:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
uv tool install git+https://github.com/loganrosen/phab-feedback.git
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For local source development:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/loganrosen/phab-feedback.git
|
|
55
|
+
cd phab-feedback
|
|
56
|
+
python3 -m pip install -e .
|
|
57
|
+
python3 -m unittest discover -s tests
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Configuration
|
|
61
|
+
|
|
62
|
+
Choose a host with `--host`, `PHAB_FEEDBACK_HOST`, or
|
|
63
|
+
`~/.config/phab-feedback/config.json`, in that order:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"host": "https://phabricator.example.com",
|
|
68
|
+
"cookie_name": "phsid"
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If no host is configured and `~/.arcrc` contains exactly one host,
|
|
73
|
+
`phab-feedback` uses it. Conduit tokens come from `PHAB_FEEDBACK_TOKEN` or the
|
|
74
|
+
matching `~/.arcrc` entry. Tokens are never accepted as command-line arguments.
|
|
75
|
+
|
|
76
|
+
Internal web actions need a logged-in browser session. Set
|
|
77
|
+
`PHAB_FEEDBACK_SESSION_COOKIE` to a complete `Cookie` header value, or to the
|
|
78
|
+
value of the configured session cookie. The value is never printed. Mozilla
|
|
79
|
+
Phabricator users can instead pass `--firefox-cookies` to discover the session
|
|
80
|
+
from a local Firefox profile; `--firefox-profile` selects a specific profile.
|
|
81
|
+
|
|
82
|
+
`XDG_CONFIG_HOME` and `PHAB_FEEDBACK_ARCRC` are respected. The config file is
|
|
83
|
+
for non-secret settings; keep tokens in `.arcrc` or the environment and session
|
|
84
|
+
cookies in the environment or browser store.
|
|
85
|
+
|
|
86
|
+
## Commands
|
|
87
|
+
|
|
88
|
+
All successful commands write JSON to stdout. Message-taking commands accept
|
|
89
|
+
exactly one of `--message`, `--message-file PATH`, or `--message-file -`.
|
|
90
|
+
When stdin is redirected, it is also accepted without an option.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Read the complete chronological feedback timeline.
|
|
94
|
+
phab-feedback timeline D123
|
|
95
|
+
|
|
96
|
+
# Post an immediate top-level revision comment through Conduit.
|
|
97
|
+
phab-feedback comment D123 --message-file reply.txt
|
|
98
|
+
|
|
99
|
+
# Create a true inline-thread reply draft, then publish it separately.
|
|
100
|
+
printf '%s\n' 'Handled in the latest update.' |
|
|
101
|
+
phab-feedback reply-inline D123 456
|
|
102
|
+
phab-feedback submit D123
|
|
103
|
+
|
|
104
|
+
# Explicitly create and publish an inline reply in one invocation.
|
|
105
|
+
phab-feedback reply-inline D123 456 --message 'Done.' --submit
|
|
106
|
+
|
|
107
|
+
# Remove an accidental top-level comment after validating its type.
|
|
108
|
+
phab-feedback remove-comment D123 789
|
|
109
|
+
|
|
110
|
+
# Create Done drafts, then submit them.
|
|
111
|
+
phab-feedback mark-done D123 456 457
|
|
112
|
+
phab-feedback submit D123
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`timeline`, `comment`, and the metadata validation used by mutations rely on
|
|
116
|
+
standard Conduit APIs. Inline reply drafting, top-level comment removal, Done
|
|
117
|
+
drafting, and draft submission use internal web endpoints available in upstream
|
|
118
|
+
Phabricator and Phorge. Those endpoints are less stable than Conduit and may
|
|
119
|
+
change between server releases.
|
|
120
|
+
|
|
121
|
+
These commands are **Mozilla-only** because they use the Review Helper
|
|
122
|
+
extension, not upstream Phabricator:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
phab-feedback mark-helpful D123 456
|
|
126
|
+
phab-feedback mark-unhelpful D123 457
|
|
127
|
+
phab-feedback request-ai-review D123
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Helpful and unhelpful ratings take effect immediately. Inline replies and Done
|
|
131
|
+
states remain drafts until `submit`; `reply-inline --submit` is the only
|
|
132
|
+
intentional combined workflow. Rating, Done, and reply actions are never
|
|
133
|
+
combined implicitly.
|
|
134
|
+
|
|
135
|
+
The repository also includes an optional thin agent skill. It contains workflow
|
|
136
|
+
and approval guidance only; the CLI remains the single implementation of all
|
|
137
|
+
deterministic behavior. Install it through the open Skills CLI:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npx skills add loganrosen/phab-feedback@phab-feedback -g
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The Skills CLI handles the supported agent-specific installation paths. The
|
|
144
|
+
Python package does not modify agent configuration or install the skill
|
|
145
|
+
automatically.
|
|
146
|
+
|
|
147
|
+
## Security
|
|
148
|
+
|
|
149
|
+
Credentials are sent only in request headers or bodies to the configured host.
|
|
150
|
+
Errors omit request bodies, tokens, and cookies. Avoid enabling shell tracing
|
|
151
|
+
while setting credential environment variables.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "phab-feedback"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CLI for Phabricator and Phorge review feedback workflows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{name = "Logan Rosen"}]
|
|
14
|
+
keywords = ["phabricator", "phorge", "code-review", "cli"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Operating System :: MacOS",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Topic :: Software Development :: Version Control",
|
|
29
|
+
"Typing :: Typed",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
phab-feedback = "phab_feedback.cli:main"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/loganrosen/phab-feedback"
|
|
37
|
+
Repository = "https://github.com/loganrosen/phab-feedback"
|
|
38
|
+
Issues = "https://github.com/loganrosen/phab-feedback/issues"
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.packages.find]
|
|
41
|
+
where = ["src"]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Conduit and internal web API clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, Mapping
|
|
9
|
+
from urllib.parse import urlencode
|
|
10
|
+
|
|
11
|
+
from .errors import APIError
|
|
12
|
+
from .transport import Transport
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConduitClient:
|
|
16
|
+
def __init__(self, host: str, token: str, transport: Transport) -> None:
|
|
17
|
+
self.host = host
|
|
18
|
+
self._token = token
|
|
19
|
+
self._transport = transport
|
|
20
|
+
|
|
21
|
+
def call(self, method: str, params: Mapping[str, Any]) -> Any:
|
|
22
|
+
conduit_params = dict(params)
|
|
23
|
+
conduit_params["__conduit__"] = {"token": self._token}
|
|
24
|
+
data = urlencode(
|
|
25
|
+
{
|
|
26
|
+
"params": json.dumps(conduit_params, separators=(",", ":")),
|
|
27
|
+
"output": "json",
|
|
28
|
+
"__conduit__": "1",
|
|
29
|
+
}
|
|
30
|
+
).encode()
|
|
31
|
+
response = self._transport.request(
|
|
32
|
+
"POST",
|
|
33
|
+
f"{self.host}/api/{method}",
|
|
34
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
35
|
+
data=data,
|
|
36
|
+
)
|
|
37
|
+
payload = _json_object(response.body, f"Conduit method {method}")
|
|
38
|
+
if payload.get("error_code"):
|
|
39
|
+
code = payload["error_code"]
|
|
40
|
+
info = str(payload.get("error_info") or "request rejected")
|
|
41
|
+
info = info.replace(self._token, "[redacted]")
|
|
42
|
+
raise APIError(f"Conduit {method} failed: {code}: {info}")
|
|
43
|
+
if "result" not in payload:
|
|
44
|
+
raise APIError(f"Conduit {method} returned no result")
|
|
45
|
+
return payload["result"]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class WebClient:
|
|
49
|
+
def __init__(self, host: str, cookie_header: str, transport: Transport) -> None:
|
|
50
|
+
self.host = host
|
|
51
|
+
self._cookie_header = cookie_header
|
|
52
|
+
self._transport = transport
|
|
53
|
+
self._csrf: str | None = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def csrf(self) -> str:
|
|
57
|
+
if self._csrf is None:
|
|
58
|
+
response = self._transport.request(
|
|
59
|
+
"GET",
|
|
60
|
+
self.host,
|
|
61
|
+
headers={"Cookie": self._cookie_header},
|
|
62
|
+
)
|
|
63
|
+
decoded = html.unescape(
|
|
64
|
+
response.body.decode("utf-8", errors="replace")
|
|
65
|
+
)
|
|
66
|
+
patterns = (
|
|
67
|
+
r'name="__csrf__"\s+value="(B@[A-Za-z0-9]+)"',
|
|
68
|
+
r'"current":"(B@[A-Za-z0-9]+)"',
|
|
69
|
+
r'"token":"(B@[A-Za-z0-9]+)"',
|
|
70
|
+
)
|
|
71
|
+
for pattern in patterns:
|
|
72
|
+
match = re.search(pattern, decoded)
|
|
73
|
+
if match:
|
|
74
|
+
self._csrf = match.group(1)
|
|
75
|
+
break
|
|
76
|
+
if self._csrf is None:
|
|
77
|
+
raise APIError("Could not extract a CSRF token from the host")
|
|
78
|
+
return self._csrf
|
|
79
|
+
|
|
80
|
+
def post(self, path: str, data: Mapping[str, Any]) -> dict[str, Any]:
|
|
81
|
+
csrf = self.csrf
|
|
82
|
+
response = self._transport.request(
|
|
83
|
+
"POST",
|
|
84
|
+
f"{self.host}{path}",
|
|
85
|
+
headers={
|
|
86
|
+
"Cookie": self._cookie_header,
|
|
87
|
+
"X-Phabricator-Csrf": csrf,
|
|
88
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
89
|
+
},
|
|
90
|
+
data=urlencode(data).encode(),
|
|
91
|
+
)
|
|
92
|
+
body = re.sub(rb"^for \(;;\);", b"", response.body)
|
|
93
|
+
payload = _json_object(body, f"Web endpoint {path}")
|
|
94
|
+
error = payload.get("error")
|
|
95
|
+
if error:
|
|
96
|
+
raise APIError(f"Web endpoint {path} failed: {error}")
|
|
97
|
+
return payload
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _json_object(body: bytes, operation: str) -> dict[str, Any]:
|
|
101
|
+
try:
|
|
102
|
+
payload = json.loads(body.decode("utf-8"))
|
|
103
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
104
|
+
raise APIError(f"{operation} returned an invalid JSON response") from error
|
|
105
|
+
if not isinstance(payload, dict):
|
|
106
|
+
raise APIError(f"{operation} returned an unexpected response")
|
|
107
|
+
return payload
|