Chapter 13 — GitLab Integration
codecraftai-restclient-gitlabis the framework's worked example of a real-world REST integration. An OpenAPI spec auto-generates hundreds of method classes; a small hand-written foundation (RestClientBase, RestApiGroup, AspectsRest) carries the shared behaviour. The chapter walks the generator pattern and the typical method shape.
The portfolio includes codecraftai-restclient-gitlab as a worked example
of integrating with a REST API at scale. GitLab's REST API
is large — hundreds of endpoints — and integrating with it
by hand would be infeasible. Instead, the package
generates the client code from an OpenAPI specification;
the hand-written part is small and shared across all
generated methods.
This chapter walks the generator pattern, the auto-generated output, the hand-written foundation, and the Aspects integration for retry behaviour. The patterns generalise to any large REST API — projects building integrations with their own external services follow the same shape.
The package layout
codecraftai-restclient-gitlab/source/packages/ccai/gitlab/
└── rest/
├── restclientbase.py ← hand-written foundation
├── restapigroup.py ← hand-written
├── aspectsrest.py ← hand-written
├── itemsresult.py ← hand-written
├── clients/
│ ├── gitlabrestclient.py ← AUTO-GENERATED main client
│ ├── groups/ ← AUTO-GENERATED operation groups
│ ├── parameters/ ← AUTO-GENERATED parameter classes
│ └── responses/ ← AUTO-GENERATED response classes
├── generation/
│ └── restclientgenerator.py ← the generator itself
└── descriptions/ ← OpenAPI YAML / JSON specs
Two halves:
- Hand-written foundation:
RestClientBase,RestApiGroup,AspectsRest,ItemsResult. Small, stable. - Auto-generated client: hundreds of files under
clients/. Regenerated when the OpenAPI spec changes; not edited by hand.
The boundary is clear and enforced by a banner at the top of every generated file.
The auto-generated banner
File:
codecraftai-restclient-gitlab/source/packages/ccai/gitlab/rest/generation/restclientgenerator.py:26
AUTO_GENERATED_BANNER = "# Auto-generated by RestClientGenerator. DO NOT EDIT.\n"
Every generated file starts with this banner. 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.
The banner is a contract: edits to generated files don't survive regeneration. Bug fixes in generated code must be fixes to the generator.
A representative auto-generated method
File:
codecraftai-restclient-gitlab/source/packages/ccai/gitlab/rest/clients/groups/branches.py:16
def deleteapiv4projectsidrepositorybranchesbranch(
self,
params: Optional[parameters.BranchesDeleteapiv4projectsidrepositorybranchesbranchParams] = None,
*,
aspects: Optional[AspectsRest] = DEFAULT_REST_ASPECTS
) -> responses.BranchesDeleteapiv4projectsidrepositorybranchesbranchResponse:
"""
Delete a branch
"""
if params is None:
params = parameters.BranchesDeleteapiv4projectsidrepositorybranchesbranchParams()
call_leaf = "/projects/{id}/repository/branches/{branch}"
path_params = params.path_params()
if path_params:
call_leaf = self.client.format_path(call_leaf, path_params)
query_params = params.query_params()
header_params = params.header_params()
body_params = params.body_params()
exp_status = [204]
resp = self.client.perform_call(
leaf=call_leaf,
method="DELETE",
params=query_params if query_params else None,
body=body_params,
exp_status=exp_status,
headers=header_params if header_params else None,
context_msg=None,
aspects=aspects,
)
result = responses.BranchesDeleteapiv4projectsidrepositorybranchesbranchResponse(resp)
return result
Five things to notice:
- The method name is the URL path.
deleteapiv4projectsidrepositorybranchesbranchis the verb plus path with/and{}and:stripped. Ugly but unambiguous — every endpoint has a unique method. - Parameters are typed. The
paramsargument has a class generated for this specific endpoint (BranchesDeleteapiv4...Params). Parameter classes carry path / query / header / body parameters. - The Aspects parameter. Per Volume 5 chapter 1's
Aspects pattern, every call accepts a behaviour
parameter. Default is
DEFAULT_REST_ASPECTS. - Status validation.
exp_status = [204]says "expect 204 No Content." The base client'sperform_callsurfaces a non-204 response as an error. - Response wrapping. The raw HTTP response is wrapped
in a typed response class
(
BranchesDeleteapiv4...Response). Test code uses the wrapper's typed accessors rather than parsing JSON.
Every generated method has this shape. The repetition is why hand-writing is infeasible — but the repetition is also why the generator works.
The hand-written foundation
RestClientBase
File:
codecraftai-restclient-gitlab/source/packages/ccai/gitlab/rest/restclientbase.py:54
def __init__(self, host: str, credential: BaseCredential, port: Optional[int] = None,
scheme: Optional[str] = "https", aspects: AspectsRest = DEFAULT_REST_ASPECTS):
"""
Initialize the REST client base.
"""
self._host = host
self._credential = credential
self._aspects = aspects
self._host_url = f"{scheme}://{self._host}"
if port is not None:
self._host_url = f"{self._host_url}:{port}"
self._session: Session = None
return
The base class:
- Holds a host, credential, scheme, optional port.
- Holds default Aspects.
- Lazily creates a
requests.Sessionfor the connection pool. - Provides
perform_call(...)(above) that every generated method uses.
The credential authentication, the connection pooling, the
retry / polling behaviour through Aspects — all happen here.
Generated methods just construct parameters and call
perform_call.
RestApiGroup
The base for the per-group classes (Branches, Issues,
Projects, etc.). Each group is a small façade over the
RestClientBase instance, providing the methods for that
endpoint group.
AspectsRest
The Aspects subclass for REST-shaped calls. Carries:
completion_timeout— total time before the call gives up.connection_timeout— time for individual TCP connections.retry_count— how many times to retry a failed call.retry_pattern— when to retry (5xx_only,any_failure,none).polling_pattern— when to poll (e.g., for async API responses).
Different calls need different aspects: a long-running CI
pipeline trigger wants a high completion_timeout; a quick
metadata query wants a low connection_timeout and an
aggressive retry on connection-failure. The Aspects
parameter on every method lets the caller tune per-call.
Authentication
The BaseCredential integration:
def authenticate(self):
"""
Apply credential to the session. Override per-credential-type.
"""
if self._session is None:
self._session = Session()
if isinstance(self._credential, BasicAuthCredential):
self._session.auth = (self._credential.username, self._credential.password)
elif isinstance(self._credential, BearerTokenCredential):
self._session.headers["Authorization"] = f"Bearer {self._credential.token}"
# ... etc
Authentication happens at session-creation time. The
credential is set on the requests session; subsequent calls
inherit it. The session-level auth means generated methods
don't need to know about credentials — they call
perform_call, which uses the already-authenticated session.
A worked test using the GitLab integration
Suppose a CI orchestration's worker test verifies a branch exists in a project:
import testbase
from ccai.gitlab.rest.clients.gitlabrestclient import GitLabRestClient
from ccai.credentials.bearertokencredential import BearerTokenCredential
@testbase.attach.parameter(landscape, identifier="landscape")
def test_branch_exists_in_project(landscape):
"""
Verify the configured branch exists in the project.
"""
creds = landscape.credentials_manager.lookup_by_name("gitlab-readonly")
bearer = BearerTokenCredential(token=creds.token)
client = GitLabRestClient(host="gitlab.internal", credential=bearer)
from ccai.gitlab.rest.clients.parameters import (
BranchesGetapiv4projectsidrepositorybranchesParams as ListBranchesParams
)
params = ListBranchesParams(
path={"id": "my-project"},
query={"search": "main"},
)
result = client.branches.getapiv4projectsidrepositorybranches(params=params)
rtnval = any(b.name == "main" for b in result.items)
assert rtnval
return
The test:
- Resolves the GitLab credential through the framework's credential manager (Volume 2 chapter 11).
- Constructs a
GitLabRestClientwith the credential. - Calls a generated method with a typed parameter.
- Iterates the typed response.
The GitLab REST API has hundreds of endpoints; the project's test exercises one or a few. The same pattern applies to any of GitLab's endpoints — the test author picks the right method and the right parameter class.
When the OpenAPI spec changes
The integration's evolution loop:
- GitLab releases a new API version with new endpoints.
- The new OpenAPI spec is downloaded.
- The generator runs against the new spec.
- New generated files appear; existing files may change (response shapes, parameter additions).
- The hand-written foundation usually doesn't need changes — the methods that change are all auto-generated.
A small caveat: when the generator itself needs to evolve (e.g., to support a new OpenAPI feature like callbacks), the generator's code is hand-written and gets edited. But the generated client code stays in sync mechanically.
What this pattern generalises to
The auto-generation 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.
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-gitlab package is one worked example; a project
integrating with its own internal REST API can use the same
shape.
Smoke vs comprehensive testing strategy
A package with hundreds of auto-generated methods raises the question: which methods do you test?
The portfolio's discipline:
- Smoke-test the foundation.
RestClientBase's authentication, connection pooling, retry behaviour, and Aspects handling are tested explicitly. Failures in these affect every generated method. - Smoke-test the generator. Run the generator against a reference OpenAPI spec; verify the output is well-formed Python.
- Smoke-test a representative method per group. One method from each major group (Branches, Issues, Projects, etc.) verifies the end-to-end happy path.
- Don't comprehensively test every method. 100% coverage of generated methods would mean writing N similar tests for N similar methods — high cost, low marginal value.
The trade-off makes peace with the realistic constraint that no team has the budget to test 500+ methods individually. Smoke-test the substrate; trust the generator for the rest.
What the next chapter does
Chapter 14 walks the Flask web-service scaffolding —
codecraftai-webservice-flask. Where GitLab integration is
the consumer side of REST (we call other people's APIs),
the Flask scaffolding is the server side (we build APIs
others can call). The chapter walks SecurityBlueprint,
RouteSecurityManager, OAuth provider integration, and the
SQLAlchemy ORM patterns.