Skip to content

Commit c2b3364

Browse files
committed
Build: pass api_client down to environment/builders/etc
For #10289 we won't have the credentials hardcoded in our settings anymore, but instead we will be provided with a dynamic token for the build to use, for this reason we now have to pass the api client object around.
1 parent 524c24c commit c2b3364

File tree

14 files changed

+104
-133
lines changed

14 files changed

+104
-133
lines changed

readthedocs/api/v2/client.py

+1-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
"""Simple client to access our API with Slumber credentials."""
22

3-
import structlog
4-
53
import requests
4+
import structlog
65
from django.conf import settings
76
from rest_framework.renderers import JSONRenderer
87
from slumber import API, serialize
@@ -71,6 +70,3 @@ def setup_api():
7170
else:
7271
log.warning('SLUMBER_USERNAME/PASSWORD settings are not set')
7372
return API(**api_config)
74-
75-
76-
api = setup_api()

readthedocs/doc_builder/backends/sphinx.py

+18-29
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from django.urls import reverse
1717
from requests.exceptions import ConnectionError
1818

19-
from readthedocs.api.v2.client import api
2019
from readthedocs.builds import utils as version_utils
2120
from readthedocs.core.utils.filesystem import safe_open
2221
from readthedocs.doc_builder.exceptions import PDFNotFound
@@ -167,33 +166,23 @@ def get_config_params(self):
167166
versions = []
168167
downloads = []
169168
subproject_urls = []
170-
# Avoid hitting database and API if using Docker build environment
171-
if settings.DONT_HIT_API:
172-
if self.project.has_feature(Feature.ALL_VERSIONS_IN_HTML_CONTEXT):
173-
versions = self.project.active_versions()
174-
else:
175-
versions = self.project.active_versions().filter(
176-
privacy_level=PUBLIC,
177-
)
178-
downloads = self.version.get_downloads(pretty=True)
179-
subproject_urls = self.project.get_subproject_urls()
180-
else:
181-
try:
182-
versions = self.project.api_versions()
183-
if not self.project.has_feature(Feature.ALL_VERSIONS_IN_HTML_CONTEXT):
184-
versions = [
185-
v
186-
for v in versions
187-
if v.privacy_level == PUBLIC
188-
]
189-
downloads = api.version(self.version.pk).get()['downloads']
190-
subproject_urls = self.project.get_subproject_urls()
191-
except ConnectionError:
192-
log.exception(
193-
'Timeout while fetching versions/downloads/subproject_urls for Sphinx context.',
194-
project_slug=self.project.slug,
195-
version_slug=self.version.slug,
196-
)
169+
try:
170+
versions = self.project.api_versions(self.api_client)
171+
if not self.project.has_feature(Feature.ALL_VERSIONS_IN_HTML_CONTEXT):
172+
versions = [v for v in versions if v.privacy_level == PUBLIC]
173+
downloads = self.api_client.version(self.version.pk).get()["downloads"]
174+
subproject_urls = [
175+
(project["slug"], project["canonical_url"])
176+
for project in self.api_client.project(self.project.pk)
177+
.subprojects()
178+
.get()["subprojects"]
179+
]
180+
except ConnectionError:
181+
log.exception(
182+
"Timeout while fetching versions/downloads/subproject_urls for Sphinx context.",
183+
project_slug=self.project.slug,
184+
version_slug=self.version.slug,
185+
)
197186

198187
build_id = self.build_env.build.get('id')
199188
build_url = None
@@ -304,7 +293,7 @@ def append_conf(self):
304293

305294
# Append config to project conf file
306295
tmpl = template_loader.get_template('doc_builder/conf.py.tmpl')
307-
rendered = tmpl.render(self.get_config_params())
296+
rendered = tmpl.render(self.get_config_params(self.api_client))
308297

309298
with outfile:
310299
outfile.write('\n')

readthedocs/doc_builder/base.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ def __init__(self, build_env, python_env):
3737
self.project = build_env.project
3838
self.config = python_env.config if python_env else None
3939
self.project_path = self.project.checkout_path(self.version.slug)
40+
self.api_client = self.build_env.api_client
4041

4142
def get_final_doctype(self):
4243
"""Some builders may have a different doctype at build time."""
@@ -52,11 +53,8 @@ def build(self):
5253
def _post_build(self):
5354
"""Execute extra steps (e.g. create ZIP, rename PDF, etc) after building if required."""
5455

55-
def docs_dir(self, docs_dir=None, **__):
56+
def docs_dir(self):
5657
"""Handle creating a custom docs_dir if it doesn't exist."""
57-
if docs_dir:
58-
return docs_dir
59-
6058
for doc_dir_name in ['docs', 'doc', 'Doc', 'book']:
6159
possible_path = os.path.join(self.project_path, doc_dir_name)
6260
if os.path.exists(possible_path):

readthedocs/doc_builder/director.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ def create_vcs_environment(self):
121121
# Force the ``container_image`` to use one that has the latest
122122
# ca-certificate package which is compatible with Lets Encrypt
123123
container_image=settings.RTD_DOCKER_BUILD_SETTINGS["os"]["ubuntu-20.04"],
124+
api_client=self.data.api_client,
124125
)
125126

126127
def create_build_environment(self):
@@ -132,6 +133,7 @@ def create_build_environment(self):
132133
build=self.data.build,
133134
environment=self.get_build_env_vars(),
134135
use_gvisor=use_gvisor,
136+
api_client=self.data.api_client,
135137
)
136138

137139
def setup_environment(self):
@@ -588,7 +590,7 @@ def build_docs_class(self, builder_class):
588590
)
589591

590592
if builder_class == self.data.config.doctype:
591-
builder.append_conf()
593+
builder.append_conf(self.data.api_client)
592594
self.data.version.documentation_type = builder.get_final_doctype()
593595

594596
success = builder.build()

readthedocs/doc_builder/environments.py

+15-8
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from requests.exceptions import ConnectionError, ReadTimeout
1818
from requests_toolbelt.multipart.encoder import MultipartEncoder
1919

20-
from readthedocs.api.v2.client import api as api_v2
2120
from readthedocs.builds.models import BuildCommandResultMixin
2221
from readthedocs.core.utils import slugify
2322
from readthedocs.projects.models import Feature
@@ -227,7 +226,7 @@ def get_command(self):
227226
return ' '.join(self.command)
228227
return self.command
229228

230-
def save(self):
229+
def save(self, api_client):
231230
"""Save this command and result via the API."""
232231
# Force record this command as success to avoid Build reporting errors
233232
# on commands that are just for checking purposes and do not interferes
@@ -251,7 +250,7 @@ def save(self):
251250
encoder = MultipartEncoder(
252251
{key: str(value) for key, value in data.items()}
253252
)
254-
resource = api_v2.command
253+
resource = api_client.command
255254
resp = resource._store["session"].post(
256255
resource._store["base_url"] + "/",
257256
data=encoder,
@@ -261,7 +260,7 @@ def save(self):
261260
)
262261
log.debug('Post response via multipart form.', response=resp)
263262
else:
264-
resp = api_v2.command.post(data)
263+
resp = api_client.command.post(data)
265264
log.debug('Post response via JSON encoded data.', response=resp)
266265

267266

@@ -416,9 +415,12 @@ class BaseBuildEnvironment:
416415
:param build: Build instance
417416
:param environment: shell environment variables
418417
:param record: whether or not record a build commands in the databse via
419-
the API. The only case where we want this to be `False` is when
420-
instantiating this class from `sync_repository_task` because it's a
421-
background task that does not expose commands to the user.
418+
the API. The only case where we want this to be `False` is when
419+
instantiating this class from `sync_repository_task` because it's a
420+
background task that does not expose commands to the user.
421+
:param api_client: API v2 client instance (readthedocs.v2.client).
422+
This is used to record commands in the database, if `record=True`
423+
this argument is required.
422424
"""
423425

424426
def __init__(
@@ -429,6 +431,7 @@ def __init__(
429431
config=None,
430432
environment=None,
431433
record=True,
434+
api_client=None,
432435
**kwargs,
433436
):
434437
self.project = project
@@ -438,6 +441,10 @@ def __init__(
438441
self.build = build
439442
self.config = config
440443
self.record = record
444+
self.api_client = api_client
445+
446+
if self.record and not self.api_client:
447+
raise ValueError("api_client is required when record=True")
441448

442449
# TODO: remove these methods, we are not using LocalEnvironment anymore. We
443450
# need to find a way for tests to not require this anymore
@@ -449,7 +456,7 @@ def __exit__(self, exc_type, exc_value, tb):
449456

450457
def record_command(self, command):
451458
if self.record:
452-
command.save()
459+
command.save(self.api_client)
453460

454461
def run(self, *cmd, **kwargs):
455462
"""Shortcut to run command from environment."""

readthedocs/projects/models.py

+10-24
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
from django_extensions.db.models import TimeStampedModel
2626
from taggit.managers import TaggableManager
2727

28-
from readthedocs.api.v2.client import api
2928
from readthedocs.builds.constants import EXTERNAL, INTERNAL, LATEST, STABLE
3029
from readthedocs.constants import pattern_opts
3130
from readthedocs.core.history import ExtraHistoricalRecords
@@ -562,23 +561,6 @@ def get_builds_url(self):
562561
},
563562
)
564563

565-
def get_canonical_url(self):
566-
if settings.DONT_HIT_DB:
567-
return api.project(self.pk).canonical_url().get()['url']
568-
return self.get_docs_url()
569-
570-
def get_subproject_urls(self):
571-
"""
572-
List subproject URLs.
573-
574-
This is used in search result linking
575-
"""
576-
if settings.DONT_HIT_DB:
577-
return [(proj['slug'], proj['canonical_url']) for proj in
578-
(api.project(self.pk).subprojects().get()['subprojects'])]
579-
return [(proj.child.slug, proj.child.get_docs_url())
580-
for proj in self.subprojects.all()]
581-
582564
def get_storage_paths(self):
583565
"""
584566
Get the paths of all artifacts used by the project.
@@ -1047,13 +1029,17 @@ def get_latest_build(self, finished=True):
10471029
kwargs['state'] = 'finished'
10481030
return self.builds(manager=INTERNAL).filter(**kwargs).first()
10491031

1050-
def api_versions(self):
1032+
def api_versions(self, api_client):
1033+
"""
1034+
Get active versions from the API.
1035+
1036+
:param api_client: API v2 client instance (readthedocs.v2.client).
1037+
"""
10511038
from readthedocs.builds.models import APIVersion
1052-
ret = []
1053-
for version_data in api.project(self.pk).active_versions.get()['versions']:
1054-
version = APIVersion(**version_data)
1055-
ret.append(version)
1056-
return sort_version_aware(ret)
1039+
1040+
versions_data = api_client.project(self.pk).active_versions.get()["versions"]
1041+
versions = [APIVersion(**version_data) for version_data in versions_data]
1042+
return sort_version_aware(versions)
10571043

10581044
def active_versions(self):
10591045
from readthedocs.builds.models import Version

readthedocs/projects/tasks/builds.py

+19-16
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
from celery import Task
1414
from django.conf import settings
1515
from django.utils import timezone
16+
from slumber import API
1617
from slumber.exceptions import HttpClientError
1718

18-
from readthedocs.api.v2.client import api as api_v2
19+
from readthedocs.api.v2.client import setup_api
1920
from readthedocs.builds import tasks as build_tasks
2021
from readthedocs.builds.constants import (
2122
ARTIFACT_TYPES,
@@ -102,6 +103,9 @@ class TaskData:
102103
build_pk: int = None
103104
build_commit: str = None
104105

106+
# Slumber client to interact with the API v2.
107+
api_client: API = None
108+
105109
start_time: timezone.datetime = None
106110
# pylint: disable=unsubscriptable-object
107111
environment_class: type[DockerBuildEnvironment] | type[LocalBuildEnvironment] = None
@@ -153,6 +157,8 @@ def before_start(self, task_id, args, kwargs):
153157
# argument
154158
self.data.version_pk = args[0]
155159

160+
self.data.api_client = setup_api()
161+
156162
# load all data from the API required for the build
157163
self.data.version = self.get_version(self.data.version_pk)
158164
self.data.project = self.data.version.project
@@ -339,8 +345,10 @@ def sigint_received(*args, **kwargs):
339345

340346
def _check_concurrency_limit(self):
341347
try:
342-
response = api_v2.build.concurrent.get(project__slug=self.data.project.slug)
343-
concurrency_limit_reached = response.get('limit_reached', False)
348+
response = self.data.api_client.build.concurrent.get(
349+
project__slug=self.data.project.slug
350+
)
351+
concurrency_limit_reached = response.get("limit_reached", False)
344352
max_concurrent_builds = response.get(
345353
'max_concurrent',
346354
settings.RTD_MAX_CONCURRENT_BUILDS,
@@ -390,6 +398,8 @@ def before_start(self, task_id, args, kwargs):
390398
# anymore and we are not using it
391399
self.data.environment_class = LocalBuildEnvironment
392400

401+
self.data.api_client = setup_api()
402+
393403
self.data.build = self.get_build(self.data.build_pk)
394404
self.data.version = self.get_version(self.data.version_pk)
395405
self.data.project = self.data.version.project
@@ -427,7 +437,7 @@ def _reset_build(self):
427437
# Reset build only if it has some commands already.
428438
if self.data.build.get("commands"):
429439
log.info("Resetting build.")
430-
api_v2.build(self.data.build["id"]).reset.post()
440+
self.data.api_client.build(self.data.build["id"]).reset.post()
431441

432442
def on_failure(self, exc, task_id, args, kwargs, einfo):
433443
"""
@@ -598,7 +608,7 @@ def on_success(self, retval, task_id, args, kwargs):
598608
# TODO: remove this condition and *always* update the DB Version instance
599609
if "html" in valid_artifacts:
600610
try:
601-
api_v2.version(self.data.version.pk).patch(
611+
self.data.api_client.version(self.data.version.pk).patch(
602612
{
603613
"built": True,
604614
"documentation_type": self.data.version.documentation_type,
@@ -709,7 +719,7 @@ def update_build(self, state=None):
709719
# self.data.build[key] = val.decode('utf-8', 'ignore')
710720

711721
try:
712-
api_v2.build(self.data.build['id']).patch(self.data.build)
722+
self.data.api_client.build(self.data.build["id"]).patch(self.data.build)
713723
except Exception:
714724
# NOTE: we are updating the "Build" object on each `state`.
715725
# Only if the last update fails, there may be some inconsistency
@@ -802,22 +812,15 @@ def save_build_data(self):
802812
except Exception:
803813
log.exception("Error while saving build data")
804814

805-
@staticmethod
806-
def get_project(project_pk):
807-
"""Get project from API."""
808-
project_data = api_v2.project(project_pk).get()
809-
return APIProject(**project_data)
810-
811-
@staticmethod
812-
def get_build(build_pk):
815+
def get_build(self, build_pk):
813816
"""
814817
Retrieve build object from API.
815818
816819
:param build_pk: Build primary key
817820
"""
818821
build = {}
819822
if build_pk:
820-
build = api_v2.build(build_pk).get()
823+
build = self.data.api_client.build(build_pk).get()
821824
private_keys = [
822825
'project',
823826
'version',
@@ -834,7 +837,7 @@ def get_build(build_pk):
834837
# build has finished to reduce API calls.
835838
def set_valid_clone(self):
836839
"""Mark on the project that it has been cloned properly."""
837-
api_v2.project(self.data.project.pk).patch(
840+
self.data.api_client.project(self.data.project.pk).patch(
838841
{'has_valid_clone': True}
839842
)
840843
self.data.project.has_valid_clone = True

0 commit comments

Comments
 (0)