Chapter 7 — The Auto-Generated GitHub Public-API Client
GitHub's public REST API has hundreds of endpoints. Hand-writing a client for it is infeasible.
codecraftai-restclient-github-publicis the worked example of auto-generation: 571 generated modules built on the foundation from chapter 6. This chapter walks what's worth testing in such a package and what to trust by construction.
codecraftai-restclient-github-public is the largest package in
the volume by file count — roughly 571 Python modules —
because GitHub's API is enormous. The package is a practical
demonstration that the foundation classes from chapter 6
scale. The chapter walks how a generated client is
structured, what tests exercise it, and the
smoke-vs-comprehensive testing strategy the framework
recommends for any auto-generated client at this scale.
What's in the package
codecraftai-restclient-github-public/source/packages/ccai/
├── github/
│ └── rest/
│ └── clients/
│ └── public/
│ ├── githubpublicrestclient.py # the entry-point client
│ ├── groups/ # 42 group files
│ ├── schemas/ # ~525 schema files
│ └── ...
└── descriptions/ # 76 OpenAPI JSON spec files
Counting:
- 571 Python modules — generated from the OpenAPI spec.
- 525 schema classes — one per JSON schema in the spec.
- 42 operation groups — one per logical API tag (repos, issues, pulls, actions, etc.).
- 76 spec JSON files — the OpenAPI source split into manageable chunks.
The package is mechanically generated. Edits to generated files don't survive regeneration.
The auto-generated banner
Every generated file starts with:
# Auto-generated by RestClientGenerator. DO NOT EDIT.
This banner is the contract. An engineer attempting to fix a bug in a generated method by editing the generated file would see the banner and know to fix the generator (or the OpenAPI spec) instead. Edits to generated files do not survive regeneration.
The banner is also a useful greppable marker. A reader exploring the codebase can quickly distinguish hand-written code (no banner) from generated code (banner).
A representative generated method
File:
source/packages/ccai/github/rest/clients/public/groups/repos.py:84–162
def accept_invitation_for_authenticated_user(
self,
invitation_id: int,
*,
exp_status: Optional[List[int]] = None,
headers: Optional[Dict[str, str]] = None,
aspects: Optional[AspectsRest] = None,
) -> Any:
"""
Accept a repository invitation
"""
client = self.client
if aspects is None:
aspects = client.aspects
header_params = client.headers
if headers is not None:
header_params = headers
leaf_template = "/user/repository_invitations/{invitation_id}"
path_params = {
'invitation_id': invitation_id,
}
call_leaf = leaf_template.format(**path_params)
query_params = None
if exp_status is None:
exp_status = [204]
resp = client.perform_call(
leaf=call_leaf,
method="PATCH",
params=query_params if query_params else None,
body=None,
exp_status=exp_status,
headers=header_params if header_params else None,
context_msg=None,
aspects=aspects,
)
return resp.json()
A more complex generated method that takes a body and parses the response into typed schema objects:
def add_app_access_restrictions(
self,
owner: str, repo: str, branch: str,
body: Dict[str, Any],
*,
exp_status: Optional[List[int]] = None,
headers: Optional[Dict[str, str]] = None,
aspects: Optional[AspectsRest] = None,
) -> List[Integration]:
"""
Add app access restrictions
Protected branches are available in public repositories
with GitHub Free and GitHub Free for organizations, ...
"""
client = self.client
if aspects is None:
aspects = client.aspects
header_params = client.headers
if headers is not None:
header_params = headers
leaf_template = "/repos/{owner}/{repo}/branches/{branch}/protection/restrictions/apps"
path_params = {
'owner': owner,
'repo': repo,
'branch': branch,
}
call_leaf = leaf_template.format(**path_params)
query_params = None
if exp_status is None:
exp_status = [200]
resp = client.perform_call(
leaf=call_leaf,
method="POST",
params=query_params if query_params else None,
body=body,
exp_status=exp_status,
headers=header_params if header_params else None,
context_msg=None,
aspects=aspects,
)
raw_data = resp.json()
items = [self._parse_item(item, Integration) for item in raw_data]
return items
Five steps repeat across hundreds of methods:
- Resolve aspects (use caller's, fall back to client's default).
- Format the URL path with caller-supplied parameters.
- Determine expected statuses (allow caller override).
- Call
client.perform_call(chapter 6's foundation). - Parse the response —
resp.json()for opaque returns,_parse_item(...)for typed-schema returns.
The repetition is the point of generation. Every method follows the same template; the generator instantiates the template per OpenAPI operation. Reviewers who can read one method can read all of them.
The entry-point client
File:
source/packages/ccai/github/rest/clients/public/githubpublicrestclient.py:53–70
class GithubPublicRestClient(GithubRestClientBase):
"""
GitHub public REST API client generated from the
OpenAPI specification.
"""
def __init__(self, host: str, credential: BaseCredential,
port: Optional[int] = None,
scheme: Optional[str] = "https",
aspects: AspectsRest = DEFAULT_REST_ASPECTS):
super().__init__(host, credential, port, scheme=scheme, aspects=aspects)
self._grp_actions = ActionsApiGroup(self)
self._grp_activity = ActivityApiGroup(self)
self._grp_apps = AppsApiGroup(self)
self._grp_billing = BillingApiGroup(self)
# ... 38 more groups instantiated as attributes ...
self._grp_repos = ReposApiGroup(self)
# ...
The client's constructor:
- Calls the GitHub-specific base class's constructor
(
GithubRestClientBaseadds theAccept: application/vnd.github.v3+jsonheader). - Instantiates each of the 42 API groups as a private attribute.
- Exposes each as a public property:
@property
def actions(self) -> "ActionsApiGroup":
return self._grp_actions
@property
def repos(self) -> "ReposApiGroup":
return self._grp_repos
# ... etc.
Test code uses the groups directly:
client = GithubPublicRestClient("api.github.com", bearer_credential)
result = client.repos.list_user_repos(username="alice")
The flow: client → group → method → perform_call →
HTTP → response → parsed schema → returned to test. Every
generated method follows the same path.
The hand-written customisation point
File: githubrestclientbase.py:34–49
class GithubRestClientBase(RestClientBase):
"""
GitHub-specific REST client base.
Adds the GitHub Accept header for API versioning.
"""
def __init__(self, host: str, credential: BaseCredential,
port: Optional[int] = None,
scheme: Optional[str] = "https",
aspects: AspectsRest = DEFAULT_REST_ASPECTS):
# GitHub-specific: pin to API v3 via Accept header.
headers = {
"Accept": "application/vnd.github.v3+json",
}
super().__init__(
host=host, credential=credential, port=port, scheme=scheme,
headers=headers, aspects=aspects,
)
return
This is the one hand-written class that customises the auto-generated client. Two things it does:
- Adds the
Acceptheader to pin to GitHub API v3. - Inherits everything else from
RestClientBase.
The customisation point is small because GitHub's REST API
is well-behaved — most of its specifics are already in the
OpenAPI spec the generator consumed. A different API might
need more customisation (custom auth flow, request signing,
etc.); the pattern is the same — subclass RestClientBase,
add what's specific.
What gets tested
A fair question for a 571-module package: which methods do you test?
The portfolio's discipline is smoke-vs-comprehensive:
Smoke-test the foundation classes
RestClientBase, AspectsRest, RestApiGroup,
ItemsResult, RestClientGenerator get explicit unit tests.
Failures in these affect every generated method, so testing
them is high-leverage.
Smoke-test the generator itself
The generator is run against a reference OpenAPI spec; the output is verified to be well-formed Python (parses, imports, types check). This catches generator bugs before they propagate to hundreds of generated methods.
Smoke-test a representative method per group
One method from each of the 42 GitHub groups (repos / issues / pulls / actions / etc.) is exercised as an integration test. This catches:
- Authentication issues affecting that group.
- Path templating issues.
- Response parsing issues for that group's schemas.
The chosen methods are typically idempotent (GET requests, not POSTs that mutate state) and well-known (e.g., "get the authenticated user" rather than an obscure endpoint).
Don't comprehensively test every method
Writing 571 individual method tests would be:
- Expensive to write — at least a day per method to cover happy path, error cases, parameter variations.
- Expensive to maintain — every spec update could require updates to every test.
- Low marginal value — methods fail in patterns, not
individually. A change to
perform_callthat breaks one method probably breaks them all; testing one catches it.
The trade-off makes peace with the realistic budget constraint. Smoke-test the substrate; trust the generator for the rest; rely on integration tests at the next layer (tests that use the GitHub client to exercise specific workflows) to catch real bugs.
When the API spec changes
GitHub releases new API versions. The integration's evolution loop:
- Update the OpenAPI spec. Drop the new spec into
descriptions/. - Run the generator.
RestClientGeneratorregenerates the schemas, groups, and client. - Review generator diffs. New methods appear; existing methods may have parameter additions; response shapes may change.
- Run the smoke tests. Most should still pass; failures indicate spec changes that need consumer-side updates.
- Hand-written code usually doesn't change.
GithubRestClientBase(the API-version pin) only changes when GitHub deprecates v3.
A small caveat: when the generator itself needs to evolve (e.g., to support a new OpenAPI feature like callbacks or webhooks), the generator's code is hand-written and gets edited. But the generated client code stays in sync mechanically.
What this pattern generalises to
Volume 6 chapter 13 walked GitLab integration using the same pattern. The pattern works whenever an external API provides a machine-readable specification:
- OpenAPI / Swagger for REST APIs.
- Protocol Buffers for gRPC APIs.
- WSDL for SOAP services.
- AsyncAPI for event-driven APIs.
- GraphQL schema for GraphQL APIs (with appropriate generator extensions).
For each, the framework's pattern is the same: hand-write a
small shared foundation, write a generator that consumes the
spec, regenerate when the spec changes. The
codecraftai-restclient-github-public package is one worked
example of an OpenAPI-driven generator at scale.
Reading order for a contributor
A reader wanting to contribute to the GitHub client (or fork it for a similar API) should read in this order:
RestClientBase(chapter 6) — the foundation.RestClientGeneratorincodecraftai-restclient-base— how generation works.GithubRestClientBase— the hand-written customisation.- One representative group (e.g.,
groups/repos.py) — to see what the generator produces. - One representative schema (e.g.,
schemas/Repository) — to see how typed parsing works.
By the end, the reader can run the generator against an updated spec and ship the new client.
What this chapter set up
A short chapter for a large package. The reader should now:
- Understand auto-generation as a viable pattern for large REST APIs.
- Recognise the auto-generated banner as the signal that a file isn't hand-edited.
- Know the smoke-vs-comprehensive testing strategy.
- See the connection between
codecraftai-restclient-github-public(this chapter) andcodecraftai-restclient-gitlab(Volume 6 chapter 13) — same pattern, different APIs.
The next chapter walks the four protocol extension packages
— UPnP, Tasker, Bluetooth, CAN. Where this chapter walked
one large auto-generated package, chapter 8 walks four
smaller hand-written packages, each adding a protocol the
core codecraftai-interop doesn't ship.