hidden-moves-github-projects 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,327 @@
1
+ """Two fixed mutations, guarded by trusted local Project authorization."""
2
+
3
+ from collections.abc import Iterable
4
+ from dataclasses import dataclass, replace
5
+ from typing import Literal
6
+
7
+ from . import _normalize as normalize
8
+ from .client import ProjectsClient, _count, _text
9
+ from .errors import CredentialError, PaginationLimitError, ProjectsError, ProtocolError
10
+ from .models import ProjectField, SelectOption
11
+
12
+ CONTENT_QUERY = """query WritableContent($id: ID!) {
13
+ node(id: $id) { __typename id }
14
+ }"""
15
+ MEMBERSHIP_QUERY = """query ProjectItemMembership($id: ID!) {
16
+ node(id: $id) { __typename ... on ProjectV2Item {
17
+ id project { id } content { __typename ... on Issue { id } ... on PullRequest { id } }
18
+ } }
19
+ }"""
20
+ SELECTION_QUERY = """query SingleSelectValue($id: ID!, $name: String!) {
21
+ node(id: $id) { __typename ... on ProjectV2Item {
22
+ id project { id } fieldValueByName(name: $name) {
23
+ __typename ... on ProjectV2ItemFieldSingleSelectValue {
24
+ optionId field { ... on ProjectV2SingleSelectField { id } }
25
+ }
26
+ }
27
+ } }
28
+ }"""
29
+ ADD_MUTATION = """mutation AddProjectItem($project: ID!, $content: ID!) {
30
+ addProjectV2ItemById(input: {projectId: $project, contentId: $content}) { item { id } }
31
+ }"""
32
+ SET_MUTATION = """mutation SetSingleSelect($project: ID!, $item: ID!, $field: ID!, $option: String!) {
33
+ updateProjectV2ItemFieldValue(input: {projectId: $project, itemId: $item, fieldId: $field,
34
+ value: {singleSelectOptionId: $option}}) { projectV2Item { id } }
35
+ }"""
36
+
37
+
38
+ class ProjectAuthorizationError(ProjectsError):
39
+ """The configured allowlist, content type or item membership denies the write."""
40
+
41
+
42
+ class SelectionConflictError(ProjectsError):
43
+ """The single-select value changed before the preflight check."""
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class WriteReceipt:
48
+ operation: Literal["add_item", "set_single_select"]
49
+ project_id: str
50
+ item_id: str | None
51
+ content_id: str | None
52
+ field_id: str | None
53
+ option_id: str | None
54
+ verified: bool
55
+ acknowledged: bool = False
56
+ diagnostic: str | None = None
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class SingleSelectState:
61
+ project_id: str
62
+ item_id: str
63
+ field_id: str
64
+ current_option_id: str | None
65
+ options: list[SelectOption]
66
+
67
+
68
+ class WriteOutcomeUnknownError(ProjectsError):
69
+ """A submitted mutation may have taken effect; inspect before any retry."""
70
+
71
+ def __init__(self, receipt: WriteReceipt, *, acknowledged: bool, cause_type: str):
72
+ self.receipt = replace(
73
+ receipt, acknowledged=acknowledged, diagnostic=cause_type
74
+ )
75
+ self.acknowledged = acknowledged
76
+ self.cause_type = cause_type
77
+ super().__init__(
78
+ "mutation outcome is unverified; inspect the Project before retrying"
79
+ )
80
+
81
+
82
+ class PostWriteVerificationError(WriteOutcomeUnknownError):
83
+ """GitHub acknowledged the mutation but its post-write read did not verify it."""
84
+
85
+
86
+ def _opaque(value: str, label: str) -> str:
87
+ value = _text(value, label)
88
+ if len(value) > 1024 or any(char.isspace() for char in value):
89
+ raise ValueError(f"{label} must be a bounded opaque ID")
90
+ return value
91
+
92
+
93
+ class ProjectsWriter:
94
+ """Trusted configuration authorizes Projects; tool input cannot add permission."""
95
+
96
+ def __init__(
97
+ self,
98
+ reader: ProjectsClient,
99
+ *,
100
+ allowed_projects: Iterable[str],
101
+ max_field_pages: int = 5,
102
+ ):
103
+ if not isinstance(reader, ProjectsClient):
104
+ raise TypeError("reader must be an explicitly configured ProjectsClient")
105
+ if isinstance(allowed_projects, (str, bytes)):
106
+ raise ValueError("allowed_projects must be an explicit collection of IDs")
107
+ projects = set()
108
+ for index, project_id in enumerate(allowed_projects):
109
+ if index >= 64:
110
+ raise ValueError("authorize at most 64 Projects per local writer")
111
+ projects.add(_opaque(project_id, "project_id"))
112
+ if not projects:
113
+ raise ValueError("authorize at least one explicit Project")
114
+ self._reader = reader
115
+ self._allowed_projects = frozenset(projects)
116
+ self._max_field_pages = _count(max_field_pages, "max_field_pages", 100)
117
+
118
+ def __repr__(self) -> str:
119
+ return "ProjectsWriter()"
120
+
121
+ def _authorize(self, project_id: str) -> None:
122
+ _opaque(project_id, "project_id")
123
+ if project_id not in self._allowed_projects:
124
+ raise ProjectAuthorizationError(
125
+ "Project is outside the configured write allowlist"
126
+ )
127
+
128
+ def _member(self, node: dict, project_id: str, item_id: str) -> None:
129
+ if (
130
+ normalize.text(node.get("id")) != item_id
131
+ or normalize.text(normalize.obj(node.get("project")).get("id"))
132
+ != project_id
133
+ ):
134
+ raise ProjectAuthorizationError(
135
+ "item does not belong to the authorized Project"
136
+ )
137
+
138
+ def _field(self, project_id: str, name: str) -> ProjectField:
139
+ _text(name, "field_name")
140
+ found, cursor, seen = [], None, set()
141
+ for _ in range(self._max_field_pages):
142
+ page = self._reader.fields(project_id, cursor=cursor, page_size=100)
143
+ found.extend(field for field in page.fields if field.name == name)
144
+ if not page.page_info.has_next_page:
145
+ break
146
+ cursor = page.page_info.end_cursor
147
+ if cursor in seen:
148
+ raise ProtocolError("GitHub repeated a field-definition cursor")
149
+ seen.add(cursor)
150
+ else:
151
+ raise PaginationLimitError(
152
+ "field-definition budget ended before the connection"
153
+ )
154
+ if (
155
+ len(found) != 1
156
+ or found[0].graphql_type != "ProjectV2SingleSelectField"
157
+ or found[0].id is None
158
+ ):
159
+ raise ValueError(
160
+ "field_name must identify one accessible single-select field"
161
+ )
162
+ _opaque(found[0].id, "field_id")
163
+ return found[0]
164
+
165
+ def _selection(
166
+ self, project_id: str, item_id: str, field: ProjectField
167
+ ) -> str | None:
168
+ data = self._reader._execute(
169
+ SELECTION_QUERY, {"id": item_id, "name": field.name}
170
+ )
171
+ node = normalize.node(data, "ProjectV2Item")
172
+ self._member(node, project_id, item_id)
173
+ value = node.get("fieldValueByName")
174
+ if value is None:
175
+ return None
176
+ value = normalize.obj(value)
177
+ if (
178
+ value.get("__typename") != "ProjectV2ItemFieldSingleSelectValue"
179
+ or normalize.obj(value.get("field")).get("id") != field.id
180
+ ):
181
+ raise ProtocolError(
182
+ "single-select lookup returned a different field or value type"
183
+ )
184
+ return _opaque(normalize.text(value.get("optionId")), "option_id")
185
+
186
+ def inspect_single_select(
187
+ self, project_id: str, item_id: str, field_name: str
188
+ ) -> SingleSelectState:
189
+ """Read the authorized item value and dynamically resolved choices."""
190
+ self._authorize(project_id)
191
+ _opaque(item_id, "item_id")
192
+ field = self._field(project_id, field_name)
193
+ return SingleSelectState(
194
+ project_id,
195
+ item_id,
196
+ field.id,
197
+ self._selection(project_id, item_id, field),
198
+ field.options,
199
+ )
200
+
201
+ def _mutate(self, query: str, variables: dict, receipt: WriteReceipt) -> dict:
202
+ try:
203
+ return self._reader._execute(query, variables)
204
+ except CredentialError:
205
+ raise
206
+ except ProjectsError as error:
207
+ # Never retry automatically: transport/GraphQL errors can follow a completed effect.
208
+ raise WriteOutcomeUnknownError(
209
+ receipt, acknowledged=False, cause_type=type(error).__name__
210
+ ) from None
211
+
212
+ def add_item(self, project_id: str, content_id: str) -> WriteReceipt:
213
+ """Add one existing Issue/PR and verify returned identity and membership."""
214
+ self._authorize(project_id)
215
+ _opaque(content_id, "content_id")
216
+ self._reader.fields(
217
+ project_id, page_size=1
218
+ ) # also verifies Project read access/type
219
+ content = normalize.obj(
220
+ self._reader._execute(CONTENT_QUERY, {"id": content_id}).get("node")
221
+ )
222
+ if (
223
+ content.get("__typename") not in ("Issue", "PullRequest")
224
+ or content.get("id") != content_id
225
+ ):
226
+ raise ProjectAuthorizationError(
227
+ "content must be an accessible existing Issue or PullRequest"
228
+ )
229
+ receipt = WriteReceipt(
230
+ "add_item", project_id, None, content_id, None, None, False
231
+ )
232
+ data = self._mutate(
233
+ ADD_MUTATION, {"project": project_id, "content": content_id}, receipt
234
+ )
235
+ try:
236
+ item_id = _opaque(
237
+ normalize.text(
238
+ normalize.obj(
239
+ normalize.obj(data.get("addProjectV2ItemById")).get("item")
240
+ ).get("id")
241
+ ),
242
+ "item_id",
243
+ )
244
+ except (ProjectsError, ValueError) as error:
245
+ raise WriteOutcomeUnknownError(
246
+ receipt, acknowledged=False, cause_type=type(error).__name__
247
+ ) from None
248
+ receipt = replace(receipt, item_id=item_id, acknowledged=True)
249
+ try:
250
+ node = normalize.node(
251
+ self._reader._execute(MEMBERSHIP_QUERY, {"id": item_id}),
252
+ "ProjectV2Item",
253
+ )
254
+ self._member(node, project_id, item_id)
255
+ if normalize.obj(node.get("content")).get("id") != content_id:
256
+ raise ProtocolError(
257
+ "added item content did not match the supplied identity"
258
+ )
259
+ except (ProjectsError, ValueError) as error:
260
+ raise PostWriteVerificationError(
261
+ receipt, acknowledged=True, cause_type=type(error).__name__
262
+ ) from None
263
+ return replace(receipt, verified=True, acknowledged=True)
264
+
265
+ def set_single_select(
266
+ self,
267
+ project_id: str,
268
+ item_id: str,
269
+ field_name: str,
270
+ option_name: str,
271
+ expected_option_id: str | None,
272
+ ) -> WriteReceipt:
273
+ """Check the expected value, set a dynamically resolved option, then verify."""
274
+ self._authorize(project_id)
275
+ _opaque(item_id, "item_id")
276
+ _text(option_name, "option_name")
277
+ if expected_option_id is not None:
278
+ _opaque(expected_option_id, "expected_option_id")
279
+ field = self._field(project_id, field_name)
280
+ options = [option for option in field.options if option.name == option_name]
281
+ if len(options) != 1:
282
+ raise ValueError(
283
+ "option_name must identify one choice on the resolved field"
284
+ )
285
+ option_id = _opaque(options[0].id, "option_id")
286
+ if self._selection(project_id, item_id, field) != expected_option_id:
287
+ raise SelectionConflictError(
288
+ "single-select value differs from the caller's expected option"
289
+ )
290
+ receipt = WriteReceipt(
291
+ "set_single_select", project_id, item_id, None, field.id, option_id, False
292
+ )
293
+ data = self._mutate(
294
+ SET_MUTATION,
295
+ {
296
+ "project": project_id,
297
+ "item": item_id,
298
+ "field": field.id,
299
+ "option": option_id,
300
+ },
301
+ receipt,
302
+ )
303
+ try:
304
+ returned = normalize.obj(
305
+ normalize.obj(data.get("updateProjectV2ItemFieldValue")).get(
306
+ "projectV2Item"
307
+ )
308
+ )
309
+ if returned.get("id") != item_id:
310
+ raise ProtocolError(
311
+ "mutation response did not identify the requested item"
312
+ )
313
+ except (ProjectsError, ValueError) as error:
314
+ raise WriteOutcomeUnknownError(
315
+ receipt, acknowledged=False, cause_type=type(error).__name__
316
+ ) from None
317
+ receipt = replace(receipt, acknowledged=True)
318
+ try:
319
+ if self._selection(project_id, item_id, field) != option_id:
320
+ raise ProtocolError(
321
+ "single-select post-write value did not match the requested choice"
322
+ )
323
+ except (ProjectsError, ValueError) as error:
324
+ raise PostWriteVerificationError(
325
+ receipt, acknowledged=True, cause_type=type(error).__name__
326
+ ) from None
327
+ return replace(receipt, verified=True, acknowledged=True)
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: hidden-moves-github-projects
3
+ Version: 0.1.0
4
+ Summary: Read and explicitly edit GitHub Projects through typed Python operations.
5
+ Keywords: github,projects,graphql,capabilities
6
+ Author: Ella Inng
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Dist: backpack-store>=0.1,<0.2 ; extra == 'backpack'
16
+ Requires-Dist: hidden-moves-mcp>=0.1,<0.2 ; extra == 'host'
17
+ Requires-Dist: hidden-moves>=0.1,<0.2 ; extra == 'moves'
18
+ Maintainer: Outside Labs
19
+ Requires-Python: >=3.11
20
+ Project-URL: Homepage, https://github.com/outside-labs/hidden_moves
21
+ Project-URL: Source, https://github.com/outside-labs/hidden_moves
22
+ Project-URL: Issues, https://github.com/outside-labs/hidden_moves/issues
23
+ Project-URL: Documentation, https://github.com/outside-labs/hidden_moves/blob/main/packages/hidden-moves-github-projects/README.md
24
+ Provides-Extra: backpack
25
+ Provides-Extra: host
26
+ Provides-Extra: moves
27
+ Description-Content-Type: text/markdown
28
+
29
+ # GitHub Projects client
30
+
31
+ An experimental GraphQL client with concrete typed records and explicitly
32
+ selected write operations. The ordinary Python library uses only the standard
33
+ library and does not import Hidden Moves, MCP, or an API SDK. Capability integration
34
+ is an optional extra.
35
+
36
+ The PyPI distribution is `hidden-moves-github-projects` and the Python import is
37
+ `hidden_moves_github_projects`. The optional `backpack` extra requires
38
+ `backpack-store>=0.1,<0.2`.
39
+
40
+ ```sh
41
+ python -m pip install hidden-moves-github-projects==0.1.0
42
+ ```
43
+
44
+ Its release route uses `v0.1.0-github-projects`, `release.yaml`, and environment
45
+ `pypi-github-projects`; see the
46
+ [release guide](https://github.com/outside-labs/hidden_moves/blob/main/docs/PYPI_RELEASES.md).
47
+
48
+ ## Ordinary Python
49
+
50
+ ```python
51
+ from hidden_moves_github_projects import ProjectsClient
52
+
53
+ # The application supplies its credential resolver; construction performs no I/O.
54
+ client = ProjectsClient(resolve_token, timeout=10)
55
+ project = client.get("example", "organization", 2)
56
+ fields = client.fields(project.id)
57
+ page = client.items(project.id, page_size=20, field_page_size=50)
58
+ ```
59
+
60
+ `resolve_token` is an application-owned callable returning an authorized token.
61
+ Keep credentials outside source control and tool arguments. A successful Project
62
+ query verifies access; repository access alone does not establish Projects access.
63
+ Classic-token scopes and GitHub App permissions differ. See the
64
+ [GitHub Projects API guide](https://docs.github.com/en/issues/planning-and-tracking-with-projects/automating-your-project/using-the-api-to-manage-projects).
65
+
66
+ ## Operations and identities
67
+
68
+ - `list(owner, owner_kind, cursor=None, page_size=50)` returns a Project page.
69
+ - `get(owner, owner_kind, number)` returns a Project record.
70
+ - `fields(project_id, cursor=None, page_size=50)` returns a field-definition page.
71
+ - `items(project_id, cursor=None, page_size=50, field_page_size=50)` returns an item page.
72
+ - `item_field_values(item_id, cursor=None, page_size=50)` continues nested values.
73
+ - `iter_item_pages(project_id, ..., max_pages=10)` explicitly iterates bounded pages.
74
+
75
+ Owner kind is `user` or `organization`. Node IDs are opaque strings. Project number,
76
+ Project node ID, item node ID, content node ID, and issue/PR number are separate
77
+ identities. Item content distinguishes issues, PRs, drafts, redaction, and unknown
78
+ types. Select options retain IDs/names; iteration definitions retain active and
79
+ completed iteration metadata.
80
+
81
+ ## Paging and completeness
82
+
83
+ Each requested connection returns `page_info.has_next_page` and `end_cursor`.
84
+ Every page contains at most the requested 1–100 entries. A last page with an input
85
+ cursor still represents only that page. Nested item field values have independent
86
+ page information and `field_values_complete`; continue them with the item ID and
87
+ their own cursor. An iterator that reaches its page budget raises
88
+ `PaginationLimitError` instead of silently claiming to have read the board.
89
+
90
+ Text, number, date, single-select, and iteration values are normalized. Unsupported
91
+ value types remain visible as typed placeholders and diagnostics; their omitted
92
+ payloads make `field_values_complete=False`. Unknown field/content types and null
93
+ nodes also produce diagnostics. Issue assignees, labels, linked PRs, and repository
94
+ connections are not queried. No unbounded nested connection is silently returned.
95
+
96
+ ## Transport and errors
97
+
98
+ The default transport makes one HTTPS POST per explicit read, uses a finite socket
99
+ timeout, reads at most a configured byte budget, closes the response, and refuses
100
+ redirects carrying credentials. An explicit HTTPS endpoint supports application-
101
+ configured enterprise API URLs. Inject a `Transport` for synthetic or custom HTTP
102
+ behavior; it receives a `urllib.request.Request` and the timeout/byte budget and
103
+ returns `HTTPResponse(status, body)`.
104
+
105
+ HTTP failures, GraphQL errors (including HTTP 200 with partial data), malformed or
106
+ oversized responses, unavailable resources, and iterator limits have separate error
107
+ types. Upstream error text and resolver/transport exception messages are sanitized.
108
+ Requests are not retried automatically. Clients do not resolve local login state
109
+ unless the application explicitly supplies such a resolver.
110
+
111
+ ## Verification
112
+
113
+ ```sh
114
+ PYTHONPATH=packages/hidden-moves-github-projects/src \
115
+ .venv/bin/python -m unittest discover -s packages/hidden-moves-github-projects/tests -p test_client.py
116
+ ```
117
+
118
+ Tests use synthetic response fixtures, an injected transport, and blocked side
119
+ effects. CI builds and installs the actual wheel across the family platform matrix.
120
+ Live read access and capability-host interoperability are separate proofs.
121
+
122
+ ## Optional provider and local host
123
+
124
+ Install `[moves]` for the optional provider or `[host]` for the MCP executable.
125
+ For local development install the family packages together:
126
+
127
+ ```sh
128
+ uv pip install --python .venv/bin/python . ./packages/hidden-moves-mcp \
129
+ ./packages/hidden-moves-openai ./packages/hidden-moves-github-projects
130
+ hidden-moves --plugin github-projects moves show github.projects.items
131
+ ```
132
+
133
+ The zero-argument `github-projects` entry-point factory never resolves credentials
134
+ or makes a request. It advertises target-bound `github.projects.list/get/fields/items`
135
+ with read-only, non-destructive, external annotations. Generic framework inspection
136
+ needs no token; account invocation needs an application-bound `ProjectsClient`.
137
+
138
+ ```python
139
+ from hidden_moves_github_projects import ProjectsClient
140
+ from hidden_moves_github_projects.host import build_catalog, resolve_gh_token
141
+ from hidden_moves_mcp import MCPAdapter
142
+ from hidden_moves_openai import FunctionToolAdapter
143
+
144
+ client = ProjectsClient(resolve_gh_token, timeout=10)
145
+ catalog = build_catalog(client, ["github.projects.items"])
146
+ mcp = MCPAdapter(catalog)
147
+ functions = FunctionToolAdapter(catalog)
148
+ ```
149
+
150
+ `resolve_gh_token` explicitly captures the existing `github.com` login from `gh`
151
+ when a read executes. It neither prints the token nor changes login/scopes. A
152
+ local MCP host can launch the installed executable:
153
+
154
+ ```json
155
+ {
156
+ "command": "/absolute/path/to/environment/bin/hidden-moves-github-projects",
157
+ "args": ["--move", "github.projects.items", "--timeout", "10"]
158
+ }
159
+ ```
160
+
161
+ Only selected capabilities list or dispatch. The host supplies no arbitrary query,
162
+ shell, or Project-editing tool. The SDK owns stdio protocol and process cleanup.
163
+ This configured local application uses one local identity; a remote host requires
164
+ request-scoped authorization and credentials. Sync HTTP executes inline here;
165
+ concurrent remote hosting must provide its own offloading and resource strategy.
166
+
167
+ The consumer tests use a synthetic transport and compare ordinary Python, the
168
+ selected catalog, actual stdio MCP initialize/list/call, and offline function-tool
169
+ dispatch. Install the local family and run:
170
+
171
+ ```sh
172
+ python -m unittest discover -s packages/hidden-moves-github-projects/tests
173
+ ```
174
+
175
+ Inspection, fixture tests, and offline function tools require no model API call.
176
+ # Optional local snapshots
177
+
178
+ The `backpack` extra enables an explicit Projects-to-Backpack bridge. Store dated
179
+ normalized Project/item DTOs with a local UUID and source provenance, then reopen
180
+ them without GitHub access. Fetch and persistence can be invoked separately; no
181
+ snapshot read updates a board or claims to be live. See the
182
+ [snapshot guide](../../docs/PROJECT_SNAPSHOTS.md) and run
183
+ `python examples/projects_snapshot_demo.py` from the repository for the offline
184
+ proof. Backpack remains an independent package with no mandatory runtime
185
+ dependencies.
186
+ # Optional bounded writes
187
+
188
+ The separate `ProjectsWriter` and `github-projects-write` provider support adding
189
+ an existing Issue/PR and setting a dynamically resolved single-select choice.
190
+ The host authorizes explicit Project IDs and selects writes separately from the
191
+ default read profile. See the [write guide](../../docs/PROJECT_WRITES.md) for
192
+ expected-value checks, verified/unverified receipts and the remaining disposable
193
+ fixture gate. There are no bulk, deletion or archive tools.
@@ -0,0 +1,19 @@
1
+ hidden_moves_github_projects/__init__.py,sha256=981WudjuNVSGl0Db7ok9AUTnUV6ABO7RdXwG6RVR9c0,940
2
+ hidden_moves_github_projects/__main__.py,sha256=lJ-aBQgaPtD3giAFpUTx60YNQ67fj_lwL35Ue-ATB3M,136
3
+ hidden_moves_github_projects/_normalize.py,sha256=eJ_QeFajuxdYW73eRqVYhr8f6qLryNQlnWq6ZvY1U3o,7398
4
+ hidden_moves_github_projects/_queries.py,sha256=ynGDpWhPyE9Nmv19CsIudvvDTW40fsvYIntlQKeNjRE,2450
5
+ hidden_moves_github_projects/client.py,sha256=-Sm1katVEVppiVGeCmPHpxe6rQ21O8ObaAfd-prcw88,8965
6
+ hidden_moves_github_projects/errors.py,sha256=JAc4BFBj88BLXALxpTp3EdfbvrJ6X6PJ3kEEF2FBtQw,1088
7
+ hidden_moves_github_projects/host.py,sha256=eOKmzB1ra4grnBkbtc_IGsBK7e-aHZdB59QcODOIuy4,2758
8
+ hidden_moves_github_projects/integration.py,sha256=SmFZMZcoyJ0lmXhvSSzVceYh4YnS4AdfhghpvWS187M,2010
9
+ hidden_moves_github_projects/models.py,sha256=i-g0UJG_kRNsx5uNDIXpUVA6DgZLTNuh1lq8XzDNtSE,2548
10
+ hidden_moves_github_projects/snapshots.py,sha256=uy0PJz2_f76148LAELzSOd56WDrjaXREAauA8eQgfks,3697
11
+ hidden_moves_github_projects/transport.py,sha256=bONC5j0yJTR4WfxBVLIGzfOKcZDU_wAjX2glC_vsg_o,1022
12
+ hidden_moves_github_projects/write_host.py,sha256=5CjPt8T-CM3NN4LVLERfNJQLEnhxHkwiCUCXYkKj3B4,2208
13
+ hidden_moves_github_projects/write_integration.py,sha256=Fk4rtBipT3FjLpPDR51KFGBWnQGGlzX4ADr1fFtn6-w,1742
14
+ hidden_moves_github_projects/writes.py,sha256=G94xxifZY_vh0HzzvF5IhCNmpSXGisIK3NlxcxF2nQQ,12652
15
+ hidden_moves_github_projects-0.1.0.dist-info/licenses/LICENSE,sha256=MJiaArfnOql8NJRA2TI9JIYDOoiis3Rh8xQruzBRXoU,1066
16
+ hidden_moves_github_projects-0.1.0.dist-info/WHEEL,sha256=e4_1dyBeezi8ZjfxrZ3bnVOxFDa3ksqVqH0jTHkUZ3k,81
17
+ hidden_moves_github_projects-0.1.0.dist-info/entry_points.txt,sha256=WNeCIjgtRJZ8sK1OesWutOoqyX8e-aAMCidKaaPOPm8,357
18
+ hidden_moves_github_projects-0.1.0.dist-info/METADATA,sha256=5KyJPaNRbBo2hXzjslEIMtW2xu6FSIBCMc1mHducHhw,9283
19
+ hidden_moves_github_projects-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.19
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,8 @@
1
+ [console_scripts]
2
+ hidden-moves-github-projects = hidden_moves_github_projects.host:main
3
+ hidden-moves-github-projects-write = hidden_moves_github_projects.write_host:main
4
+
5
+ [hidden_moves.moves]
6
+ github-projects = hidden_moves_github_projects.integration:provide_moves
7
+ github-projects-write = hidden_moves_github_projects.write_integration:provide_write_moves
8
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ella Inng
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.