Skip to content

Commit f116534

Browse files
ewdurbindi
andauthored
Create utility to render and store transactional snapshot of simple detail page for a project (#8586)
* helper function to render and hash simple detail for a specific project * use the existing Jinja2 environment * file storage * format with black * store hashed project indexes at `/simple/<HASH>.<PROJECT_NAME>` as discussed also store unhashed index as normal * add 'simple.backend' config for tests * add a jinja renderer to pyramid_request fixture * restore tests for existing functionality * test new SimpleStorage services * license * test render_simple_index utility * Update warehouse/packaging/utils.py * Update warehouse/packaging/utils.py * fix tests, store last serial information on metadata of simple files * reformat/lint * Remove print statement * Add flush * Fix tests * Simpflify duplication in storage services Co-authored-by: Dustin Ingram <[email protected]>
1 parent 213fdf9 commit f116534

File tree

12 files changed

+493
-52
lines changed

12 files changed

+493
-52
lines changed

dev/environment

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ CAMO_KEY=insecurecamokey
1919
DOCS_URL="https://pythonhosted.org/{project}/"
2020

2121
FILES_BACKEND=warehouse.packaging.services.LocalFileStorage path=/var/opt/warehouse/packages/ url=http://localhost:9001/packages/{path}
22+
SIMPLE_BACKEND=warehouse.packaging.services.LocalSimpleStorage path=/var/opt/warehouse/simple/ url=http://localhost:9001/simple/{path}
2223
DOCS_BACKEND=warehouse.packaging.services.LocalDocsStorage path=/var/opt/warehouse/docs/
2324
SPONSORLOGOS_BACKEND=warehouse.admin.services.LocalSponsorLogoStorage path=/var/opt/warehouse/sponsorlogos/
2425

docker-compose.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
version: '3'
22

33
volumes:
4+
simple:
45
packages:
56
sponsorlogos:
67
vault:
@@ -86,6 +87,7 @@ services:
8687
- .coveragerc:/opt/warehouse/src/.coveragerc:z
8788
- packages:/var/opt/warehouse/packages
8889
- sponsorlogos:/var/opt/warehouse/sponsorlogos
90+
- simple:/var/opt/warehouse/simple
8991
- ./bin:/opt/warehouse/src/bin:z
9092
ports:
9193
- "80:8000"
@@ -98,6 +100,7 @@ services:
98100
volumes:
99101
- packages:/var/opt/warehouse/packages
100102
- sponsorlogos:/var/opt/warehouse/sponsorlogos
103+
- simple:/var/opt/warehouse/simple
101104
ports:
102105
- "9001:9001"
103106

@@ -113,6 +116,7 @@ services:
113116
environment:
114117
C_FORCE_ROOT: "1"
115118
FILES_BACKEND: "warehouse.packaging.services.LocalFileStorage path=/var/opt/warehouse/packages/ url=http://files:9001/packages/{path}"
119+
SIMPLE_BACKEND: "warehouse.packaging.services.LocalSimpleStorage path=/var/opt/warehouse/simple/ url=http://files:9001/simple/{path}"
116120

117121
static:
118122
build:

tests/conftest.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@
2525
import pytest
2626
import webtest as _webtest
2727

28+
from jinja2 import Environment, FileSystemLoader
2829
from psycopg2.errors import InvalidCatalogName
2930
from pyramid.i18n import TranslationString
3031
from pyramid.static import ManifestCacheBuster
32+
from pyramid_jinja2 import IJinja2Environment
3133
from pytest_postgresql.config import get_config
3234
from pytest_postgresql.janitor import DatabaseJanitor
3335
from sqlalchemy import event
3436

37+
import warehouse
38+
3539
from warehouse import admin, config, static
3640
from warehouse.accounts import services as account_services
3741
from warehouse.macaroons import services as macaroon_services
@@ -76,6 +80,22 @@ def metrics():
7680
)
7781

7882

83+
@pytest.fixture
84+
def jinja():
85+
dir_name = os.path.join(os.path.dirname(warehouse.__file__))
86+
87+
env = Environment(
88+
loader=FileSystemLoader(dir_name),
89+
extensions=[
90+
"jinja2.ext.i18n",
91+
"warehouse.utils.html.ClientSideIncludeExtension",
92+
],
93+
cache_size=0,
94+
)
95+
96+
return env
97+
98+
7999
class _Services:
80100
def __init__(self):
81101
self._services = defaultdict(lambda: defaultdict(dict))
@@ -98,11 +118,13 @@ def pyramid_services(metrics):
98118

99119

100120
@pytest.fixture
101-
def pyramid_request(pyramid_services):
121+
def pyramid_request(pyramid_services, jinja):
102122
dummy_request = pyramid.testing.DummyRequest()
103123
dummy_request.find_service = pyramid_services.find_service
104124
dummy_request.remote_addr = "1.2.3.4"
105125

126+
dummy_request.registry.registerUtility(jinja, IJinja2Environment, name=".jinja2")
127+
106128
def localize(message, **kwargs):
107129
ts = TranslationString(message, **kwargs)
108130
return ts.interpolate()
@@ -184,7 +206,8 @@ def app_config(database):
184206
"ratelimit.url": "memory://",
185207
"elasticsearch.url": "https://localhost/warehouse",
186208
"files.backend": "warehouse.packaging.services.LocalFileStorage",
187-
"docs.backend": "warehouse.packaging.services.LocalFileStorage",
209+
"simple.backend": "warehouse.packaging.services.LocalSimpleStorage",
210+
"docs.backend": "warehouse.packaging.services.LocalDocsStorage",
188211
"sponsorlogos.backend": "warehouse.admin.services.LocalSponsorLogoStorage",
189212
"mail.backend": "warehouse.email.services.SMTPEmailSender",
190213
"malware_check.backend": (

tests/unit/packaging/test_init.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from warehouse import packaging
1919
from warehouse.accounts.models import Email, User
2020
from warehouse.manage.tasks import update_role_invitation_status
21-
from warehouse.packaging.interfaces import IDocsStorage, IFileStorage
21+
from warehouse.packaging.interfaces import IDocsStorage, IFileStorage, ISimpleStorage
2222
from warehouse.packaging.models import File, Project, Release, Role
2323
from warehouse.packaging.tasks import ( # sync_bigquery_release_files,
2424
compute_trending,
@@ -51,7 +51,11 @@ def key_factory(keystring, iterate_on=None):
5151
lambda factory, iface, name=None: None
5252
),
5353
registry=pretend.stub(
54-
settings={"files.backend": "foo.bar", "docs.backend": "wu.tang"}
54+
settings={
55+
"files.backend": "foo.bar",
56+
"simple.backend": "bread.butter",
57+
"docs.backend": "wu.tang",
58+
}
5559
),
5660
register_origin_cache_keys=pretend.call_recorder(lambda c, **kw: None),
5761
get_settings=lambda: settings,
@@ -62,6 +66,7 @@ def key_factory(keystring, iterate_on=None):
6266

6367
assert config.register_service_factory.calls == [
6468
pretend.call(storage_class.create_service, IFileStorage),
69+
pretend.call(storage_class.create_service, ISimpleStorage),
6570
pretend.call(storage_class.create_service, IDocsStorage),
6671
]
6772
assert config.register_origin_cache_keys.calls == [

tests/unit/packaging/test_services.py

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@
2222

2323
import warehouse.packaging.services
2424

25-
from warehouse.packaging.interfaces import IDocsStorage, IFileStorage
25+
from warehouse.packaging.interfaces import IDocsStorage, IFileStorage, ISimpleStorage
2626
from warehouse.packaging.services import (
2727
GCSFileStorage,
28+
GCSSimpleStorage,
29+
GenericLocalBlobStorage,
2830
LocalDocsStorage,
2931
LocalFileStorage,
32+
LocalSimpleStorage,
3033
S3DocsStorage,
3134
S3FileStorage,
3235
)
@@ -137,6 +140,67 @@ def test_delete_already_gone(self, tmpdir):
137140
assert response is None
138141

139142

143+
class TestLocalSimpleStorage:
144+
def test_verify_service(self):
145+
assert verifyClass(ISimpleStorage, LocalSimpleStorage)
146+
147+
def test_basic_init(self):
148+
storage = LocalSimpleStorage("/foo/bar/")
149+
assert storage.base == "/foo/bar/"
150+
151+
def test_create_service(self):
152+
request = pretend.stub(
153+
registry=pretend.stub(settings={"simple.path": "/simple/one/two/"})
154+
)
155+
storage = LocalSimpleStorage.create_service(None, request)
156+
assert storage.base == "/simple/one/two/"
157+
158+
def test_gets_file(self, tmpdir):
159+
with open(str(tmpdir.join("file.txt")), "wb") as fp:
160+
fp.write(b"my test file contents")
161+
162+
storage = LocalSimpleStorage(str(tmpdir))
163+
file_object = storage.get("file.txt")
164+
assert file_object.read() == b"my test file contents"
165+
166+
def test_raises_when_file_non_existent(self, tmpdir):
167+
storage = LocalSimpleStorage(str(tmpdir))
168+
with pytest.raises(FileNotFoundError):
169+
storage.get("file.txt")
170+
171+
def test_stores_file(self, tmpdir):
172+
filename = str(tmpdir.join("testfile.txt"))
173+
with open(filename, "wb") as fp:
174+
fp.write(b"Test File!")
175+
176+
storage_dir = str(tmpdir.join("storage"))
177+
storage = LocalSimpleStorage(storage_dir)
178+
storage.store("foo/bar.txt", filename)
179+
180+
with open(os.path.join(storage_dir, "foo/bar.txt"), "rb") as fp:
181+
assert fp.read() == b"Test File!"
182+
183+
def test_stores_two_files(self, tmpdir):
184+
filename1 = str(tmpdir.join("testfile1.txt"))
185+
with open(filename1, "wb") as fp:
186+
fp.write(b"First Test File!")
187+
188+
filename2 = str(tmpdir.join("testfile2.txt"))
189+
with open(filename2, "wb") as fp:
190+
fp.write(b"Second Test File!")
191+
192+
storage_dir = str(tmpdir.join("storage"))
193+
storage = LocalSimpleStorage(storage_dir)
194+
storage.store("foo/first.txt", filename1)
195+
storage.store("foo/second.txt", filename2)
196+
197+
with open(os.path.join(storage_dir, "foo/first.txt"), "rb") as fp:
198+
assert fp.read() == b"First Test File!"
199+
200+
with open(os.path.join(storage_dir, "foo/second.txt"), "rb") as fp:
201+
assert fp.read() == b"Second Test File!"
202+
203+
140204
class TestS3FileStorage:
141205
def test_verify_service(self):
142206
assert verifyClass(IFileStorage, S3FileStorage)
@@ -479,3 +543,98 @@ def test_delete_by_prefix_with_storage_prefix(self):
479543
},
480544
),
481545
]
546+
547+
548+
class TestGCSSimpleStorage:
549+
def test_verify_service(self):
550+
assert verifyClass(ISimpleStorage, GCSSimpleStorage)
551+
552+
def test_basic_init(self):
553+
bucket = pretend.stub()
554+
storage = GCSSimpleStorage(bucket)
555+
assert storage.bucket is bucket
556+
557+
def test_create_service(self):
558+
service = pretend.stub(
559+
get_bucket=pretend.call_recorder(lambda bucket_name: pretend.stub())
560+
)
561+
request = pretend.stub(
562+
find_service=pretend.call_recorder(lambda name: service),
563+
registry=pretend.stub(settings={"simple.bucket": "froblob"}),
564+
)
565+
GCSSimpleStorage.create_service(None, request)
566+
567+
assert request.find_service.calls == [pretend.call(name="gcloud.gcs")]
568+
assert service.get_bucket.calls == [pretend.call("froblob")]
569+
570+
def test_gets_file_raises(self):
571+
storage = GCSSimpleStorage(pretend.stub())
572+
573+
with pytest.raises(NotImplementedError):
574+
storage.get("file.txt")
575+
576+
def test_stores_file(self, tmpdir):
577+
filename = str(tmpdir.join("testfile.txt"))
578+
with open(filename, "wb") as fp:
579+
fp.write(b"Test File!")
580+
581+
blob = pretend.stub(
582+
upload_from_filename=pretend.call_recorder(lambda file_path: None),
583+
exists=lambda: False,
584+
)
585+
bucket = pretend.stub(blob=pretend.call_recorder(lambda path: blob))
586+
storage = GCSSimpleStorage(bucket)
587+
storage.store("foo/bar.txt", filename)
588+
589+
assert bucket.blob.calls == [pretend.call("foo/bar.txt")]
590+
assert blob.upload_from_filename.calls == [pretend.call(filename)]
591+
592+
def test_stores_two_files(self, tmpdir):
593+
filename1 = str(tmpdir.join("testfile1.txt"))
594+
with open(filename1, "wb") as fp:
595+
fp.write(b"First Test File!")
596+
597+
filename2 = str(tmpdir.join("testfile2.txt"))
598+
with open(filename2, "wb") as fp:
599+
fp.write(b"Second Test File!")
600+
601+
blob = pretend.stub(
602+
upload_from_filename=pretend.call_recorder(lambda file_path: None),
603+
exists=lambda: False,
604+
)
605+
bucket = pretend.stub(blob=pretend.call_recorder(lambda path: blob))
606+
storage = GCSSimpleStorage(bucket)
607+
storage.store("foo/first.txt", filename1)
608+
storage.store("foo/second.txt", filename2)
609+
610+
assert bucket.blob.calls == [
611+
pretend.call("foo/first.txt"),
612+
pretend.call("foo/second.txt"),
613+
]
614+
assert blob.upload_from_filename.calls == [
615+
pretend.call(filename1),
616+
pretend.call(filename2),
617+
]
618+
619+
def test_stores_metadata(self, tmpdir):
620+
filename = str(tmpdir.join("testfile.txt"))
621+
with open(filename, "wb") as fp:
622+
fp.write(b"Test File!")
623+
624+
blob = pretend.stub(
625+
upload_from_filename=pretend.call_recorder(lambda file_path: None),
626+
patch=pretend.call_recorder(lambda: None),
627+
exists=lambda: False,
628+
)
629+
bucket = pretend.stub(blob=pretend.call_recorder(lambda path: blob))
630+
storage = GCSSimpleStorage(bucket)
631+
meta = {"foo": "bar"}
632+
storage.store("foo/bar.txt", filename, meta=meta)
633+
634+
assert blob.metadata == meta
635+
636+
637+
class TestGenericLocalBlobStorage:
638+
def test_notimplementederror(self):
639+
with pytest.raises(NotImplementedError):
640+
GenericLocalBlobStorage.create_service(pretend.stub(), pretend.stub())

0 commit comments

Comments
 (0)