Skip to content

Commit aeb9ccd

Browse files
authored
docs: new and shiny storage limit docs (#17716)
* docs: new and shiny storage limit docs Signed-off-by: William Woodruff <[email protected]> * add note about yanking Signed-off-by: William Woodruff <[email protected]> * `make translations` Signed-off-by: William Woodruff <[email protected]> * docs: add yanking Signed-off-by: William Woodruff <[email protected]> * docs/help: re-add help entries Signed-off-by: William Woodruff <[email protected]> --------- Signed-off-by: William Woodruff <[email protected]>
1 parent b006e30 commit aeb9ccd

File tree

18 files changed

+525
-342
lines changed

18 files changed

+525
-342
lines changed

dev/environment

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ WAREHOUSE_ENV=development
55
WAREHOUSE_TOKEN=insecuretoken
66
WAREHOUSE_IP_SALT="insecure himalayan pink salt"
77

8+
USERDOCS_DOMAIN="http://localhost:10000"
9+
810
TERMS_NOTIFICATION_BATCH_SIZE=0
911

1012
AWS_ACCESS_KEY_ID=foo

docs/mkdocs-user-docs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ edit_uri: blob/main/docs/user/
6161

6262
nav:
6363
- "index.md"
64+
- "Project Management":
65+
- "project-management/storage-limits.md"
66+
- "project-management/yanking.md"
6467
- "Organization Accounts":
6568
- "organization-accounts/index.md"
6669
- "organization-accounts/org-acc-faq.md"
29 KB
Loading
51.6 KB
Loading
55.4 KB
Loading
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
---
2+
title: Storage Limits
3+
---
4+
5+
PyPI imposes storage limits on the size of individually uploaded files,
6+
as well as the total size of all files in a project.
7+
8+
The current default limits are **100.0 MB** for individual files and **10.0 GB**
9+
for the entire project.
10+
11+
You can see your project's current size and storage limits on
12+
the project settings page (`https://pypi.org/manage/project/YOUR-PROJECT/settings/`):
13+
14+
![](/assets/project-size-and-limits.png)
15+
16+
## File size limits
17+
18+
By default, PyPI limits the size of individual files to **100.0 MB**.
19+
If you attempt to upload a file that exceeds this limit, you'll receive
20+
an error like the following:
21+
22+
```console
23+
Uploading sampleproject-1.2.3.tar.gz
24+
HTTPError: 400 Client Error: File too large. Limit for project 'sampleproject' is 100 MB.
25+
```
26+
27+
### Requesting a file size limit increase
28+
29+
!!! note
30+
31+
Note: All users submitting feedback, reporting issues or contributing to
32+
PyPI are expected to follow the
33+
[PSF Code of Conduct](https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md).
34+
35+
If you can't upload your project's release to PyPI because you're hitting the
36+
upload file size limit, we can sometimes increase your limit. Make sure you've
37+
uploaded at least one release for the project that's under the limit
38+
(a [developmental release version number](https://packaging.python.org/en/latest/specifications/version-specifiers/#developmental-releases) is fine). Then,
39+
[file an issue](https://github.com/pypi/support/issues/new?assignees=&labels=limit+request&template=limit-request-file.yml&title=File+Limit+Request%3A+PROJECT_NAME+-+000+MB) and tell
40+
us:
41+
42+
- A link to your project on PyPI (or TestPyPI)
43+
- The size of your release, in megabytes
44+
- Which index/indexes you need the increase for (PyPI, TestPyPI, or both)
45+
- A brief description of your project, including the reason for the additional size.
46+
47+
## Project size limits
48+
49+
By default, PyPI limits the total size of all files in a project to **10.0 GB**.
50+
If you attempt to upload a file that would exceed this limit, you'll receive
51+
an error like the following:
52+
53+
```console
54+
Uploading sampleproject-1.2.3.tar.gz
55+
HTTPError: 400 Client Error: Project size too large. Limit for project 'sampleproject' total size is 10 GB.
56+
```
57+
58+
### Freeing up storage on an existing project
59+
60+
!!! important
61+
62+
Deleting and [yanking](./yanking.md) are two different actions. Yanking a release or file
63+
does **not** free up storage space.
64+
65+
!!! warning
66+
67+
Deleting releases and files from your project is permanent and cannot be undone
68+
without administrative intervention.
69+
70+
!!! warning
71+
72+
Deletion can be very disruptive for downstream dependencies of your project,
73+
since it breaks installation for
74+
[pinned versions](https://pip.pypa.io/en/stable/topics/repeatable-installs/).
75+
76+
Before performing a deletion, we **strongly** recommend that you
77+
consider the potential impact on your downstreams.
78+
79+
If you're hitting the project size limit, you can free up storage by removing
80+
old releases or individual files from your project. To do this:
81+
82+
1. Navigate to the release management for your project: `https://pypi.org/manage/project/YOUR-PROJECT/releases/`;
83+
2. Click on `Options` next to the release you wish to delete from;
84+
- If you wish to delete the entire release, click `Delete`;
85+
- If you wish to delete individual files from the release, click `Manage`,
86+
then use each file's `Options` menu to delete it.
87+
88+
### Requesting a project size limit increase
89+
90+
!!! note
91+
92+
Note: All users submitting feedback, reporting issues or contributing to
93+
PyPI are expected to follow the
94+
[PSF Code of Conduct](https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md).
95+
96+
If you can't upload your project's release to PyPI because you're hitting the project size limit,
97+
first [remove any unnecessary releases or individual files](#freeing-up-storage-on-an-existing-project)
98+
to lower your overall project size.
99+
100+
If that is not possible, we can sometimes increase your limit. [File an issue](https://github.com/pypi/support/issues/new?assignees=&labels=limit+request&template=limit-request-project.yml&title=Project+Limit+Request%3A+PROJECT_NAME+-+00+GB) and tell us:
101+
102+
- A link to your project on PyPI (or TestPyPI)
103+
- The total size of your project, in gigabytes
104+
- A brief description of your project, including the reason for the additional size.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
title: Yanking
3+
---
4+
5+
!!! note
6+
7+
PyPI currently only supports yanking of *entire releases*, not individual files.
8+
9+
PyPI supports *yanking* as a non-destructive alternative to deletion.
10+
11+
A *yanked release* is a release that is always ignored by an installer, unless it
12+
is the only release that matches a [version specifier] (using either `==` or `===`).
13+
See [PEP 592] for more information.
14+
15+
[version specifier]: https://packaging.python.org/en/latest/specifications/version-specifiers/
16+
17+
[PEP 592]: https://peps.python.org/pep-0592/
18+
19+
## When should I yank a release?
20+
21+
Like deletion, yanking should be done sparingly since it can be disruptive to
22+
downstream users of a package.
23+
24+
Maintainers should consider yanking a release when:
25+
26+
- The release is broken or uninstallable.
27+
- The release violates its own compatibility guarantees. For example, `sampleproject 1.0.1`
28+
might be yanked if it's *unintentionally* incompatible with `sampleproject 1.0.0`.
29+
- The release contains a security vulnerability.
30+
31+
## How do I yank a release?
32+
33+
To yank a release, go to the release management page for your project:
34+
`https://pypi.org/manage/project/YOUR-PROJECT/releases/`.
35+
36+
Click on the `Options` button next to the release you wish to yank, then click `Yank`:
37+
38+
![](/assets/release-options-yank.png)
39+
40+
A modal dialogue will appear, asking you to confirm the yank and provide an
41+
optional reason for yanking:
42+
43+
![](/assets/yank-confirm-modal.png)
44+
45+
The reason, if provided, will be displayed on the release page as well
46+
as in the [index APIs](../api/index-api.md) used by installers.
47+
48+
!!! tip
49+
50+
Providing a reason for yanking is **strongly encouraged**, as it can
51+
help downstream users determine how to respond to the yank.

tests/unit/forklift/test_init.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ def test_includeme(forklift_domain, monkeypatch):
2525
_help_url = pretend.stub()
2626
monkeypatch.setattr(forklift, "_help_url", _help_url)
2727

28+
_user_docs_url = pretend.stub()
29+
monkeypatch.setattr(forklift, "_user_docs_url", _user_docs_url)
30+
2831
config = pretend.stub(
2932
get_settings=lambda: settings,
3033
include=pretend.call_recorder(lambda n: None),
@@ -56,7 +59,10 @@ def test_includeme(forklift_domain, monkeypatch):
5659
),
5760
]
5861

59-
assert config.add_request_method.calls == [pretend.call(_help_url, name="help_url")]
62+
assert config.add_request_method.calls == [
63+
pretend.call(_help_url, name="help_url"),
64+
pretend.call(_user_docs_url, name="user_docs_url"),
65+
]
6066
if forklift_domain:
6167
assert config.add_template_view.calls == [
6268
pretend.call(
@@ -90,3 +96,16 @@ def test_help_url():
9096
assert request.route_url.calls == [
9197
pretend.call("help", _host=warehouse_domain, _anchor="foo")
9298
]
99+
100+
101+
def test_user_docs_url():
102+
docs_domain = "http://example.com"
103+
request = pretend.stub(
104+
registry=pretend.stub(settings={"userdocs.domain": docs_domain}),
105+
)
106+
107+
assert forklift._user_docs_url(request, "/foo") == f"{docs_domain}/foo"
108+
assert (
109+
forklift._user_docs_url(request, "/foo", anchor="bar")
110+
== f"{docs_domain}/foo#bar"
111+
)

tests/unit/forklift/test_legacy.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -856,9 +856,7 @@ def test_fails_with_invalid_names(self, pyramid_config, db_request, name):
856856

857857
assert resp.status_code == 400
858858
assert resp.status == (
859-
"400 The name {!r} isn't allowed. "
860-
"See /the/help/url/ "
861-
"for more information."
859+
"400 The name {!r} isn't allowed. See /the/help/url/ for more information."
862860
).format(name)
863861

864862
@pytest.mark.parametrize(
@@ -1622,29 +1620,25 @@ def test_upload_fails_with_deprecated_classifier(
16221620
{"md5_digest": "bad"},
16231621
{
16241622
"sha256_digest": (
1625-
"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad"
1626-
"badbadb"
1623+
"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb"
16271624
)
16281625
},
16291626
{
16301627
"md5_digest": "bad",
16311628
"sha256_digest": (
1632-
"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad"
1633-
"badbadb"
1629+
"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb"
16341630
),
16351631
},
16361632
{
16371633
"md5_digest": _TAR_GZ_PKG_MD5,
16381634
"sha256_digest": (
1639-
"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbad"
1640-
"badbadb"
1635+
"badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb"
16411636
),
16421637
},
16431638
{
16441639
"md5_digest": "bad",
16451640
"sha256_digest": (
1646-
"4a8422abcc484a4086bdaa618c65289f749433b07eb433c51c4e37714"
1647-
"3ff5fdb"
1641+
"4a8422abcc484a4086bdaa618c65289f749433b07eb433c51c4e377143ff5fdb"
16481642
),
16491643
},
16501644
],
@@ -1722,9 +1716,7 @@ def test_upload_fails_with_invalid_file(self, pyramid_config, db_request):
17221716
resp = excinfo.value
17231717

17241718
assert resp.status_code == 400
1725-
assert resp.status == (
1726-
"400 Invalid distribution file. " "File is not a zipfile"
1727-
)
1719+
assert resp.status == ("400 Invalid distribution file. File is not a zipfile")
17281720

17291721
def test_upload_fails_end_of_file_error(
17301722
self, pyramid_config, db_request, project_service
@@ -1769,9 +1761,7 @@ def test_upload_fails_end_of_file_error(
17691761
resp = excinfo.value
17701762

17711763
assert resp.status_code == 400
1772-
assert resp.status == (
1773-
"400 Invalid distribution file. " "File is not a tarfile"
1774-
)
1764+
assert resp.status == ("400 Invalid distribution file. File is not a tarfile")
17751765

17761766
def test_upload_fails_with_too_large_file(self, pyramid_config, db_request):
17771767
user = UserFactory.create()
@@ -1800,14 +1790,21 @@ def test_upload_fails_with_too_large_file(self, pyramid_config, db_request):
18001790
),
18011791
}
18021792
)
1803-
db_request.help_url = pretend.call_recorder(lambda **kw: "/the/help/url/")
1793+
db_request.user_docs_url = pretend.call_recorder(
1794+
lambda *a, **kw: "/the/help/url/"
1795+
)
18041796

18051797
with pytest.raises(HTTPBadRequest) as excinfo:
18061798
legacy.file_upload(db_request)
18071799

18081800
resp = excinfo.value
18091801

1810-
assert db_request.help_url.calls == [pretend.call(_anchor="file-size-limit")]
1802+
assert db_request.user_docs_url.calls == [
1803+
pretend.call(
1804+
"/project-management/storage-limits",
1805+
anchor="requesting-a-file-size-limit-increase",
1806+
)
1807+
]
18111808
assert resp.status_code == 400
18121809
assert resp.status == (
18131810
"400 File too large. Limit for project 'foobar' is 100 MB. "
@@ -1847,14 +1844,21 @@ def test_upload_fails_with_too_large_project_size_default_limit(
18471844
),
18481845
}
18491846
)
1850-
db_request.help_url = pretend.call_recorder(lambda **kw: "/the/help/url/")
1847+
db_request.user_docs_url = pretend.call_recorder(
1848+
lambda *a, **kw: "/the/help/url/"
1849+
)
18511850

18521851
with pytest.raises(HTTPBadRequest) as excinfo:
18531852
legacy.file_upload(db_request)
18541853

18551854
resp = excinfo.value
18561855

1857-
assert db_request.help_url.calls == [pretend.call(_anchor="project-size-limit")]
1856+
assert db_request.user_docs_url.calls == [
1857+
pretend.call(
1858+
"/project-management/storage-limits",
1859+
anchor="requesting-a-project-size-limit-increase",
1860+
)
1861+
]
18581862
assert resp.status_code == 400
18591863
assert resp.status == (
18601864
"400 Project size too large."
@@ -1901,14 +1905,21 @@ def test_upload_fails_with_too_large_project_size_custom_limit(
19011905
),
19021906
}
19031907
)
1904-
db_request.help_url = pretend.call_recorder(lambda **kw: "/the/help/url/")
1908+
db_request.user_docs_url = pretend.call_recorder(
1909+
lambda *a, **kw: "/the/help/url/"
1910+
)
19051911

19061912
with pytest.raises(HTTPBadRequest) as excinfo:
19071913
legacy.file_upload(db_request)
19081914

19091915
resp = excinfo.value
19101916

1911-
assert db_request.help_url.calls == [pretend.call(_anchor="project-size-limit")]
1917+
assert db_request.user_docs_url.calls == [
1918+
pretend.call(
1919+
"/project-management/storage-limits",
1920+
anchor="requesting-a-project-size-limit-increase",
1921+
)
1922+
]
19121923
assert resp.status_code == 400
19131924
assert resp.status == (
19141925
"400 Project size too large."

tests/unit/test_config.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ def __init__(self):
312312
"terms.revision": "initial",
313313
"terms.notification_batch_size": 1000,
314314
"warehouse.commit": "null",
315+
"userdocs.domain": "https://docs.pypi.org",
315316
"site.name": "Warehouse",
316317
"token.two_factor.max_age": 300,
317318
"remember_device.days": 30,
@@ -355,17 +356,11 @@ def __init__(self):
355356
"pyramid_debugtoolbar.panels.versions.VersionDebugPanel",
356357
"pyramid_debugtoolbar.panels.settings.SettingsDebugPanel",
357358
"pyramid_debugtoolbar.panels.headers.HeaderDebugPanel",
358-
(
359-
"pyramid_debugtoolbar.panels.request_vars."
360-
"RequestVarsDebugPanel"
361-
),
359+
("pyramid_debugtoolbar.panels.request_vars.RequestVarsDebugPanel"),
362360
"pyramid_debugtoolbar.panels.renderings.RenderingsDebugPanel",
363361
"pyramid_debugtoolbar.panels.session.SessionDebugPanel",
364362
"pyramid_debugtoolbar.panels.logger.LoggingPanel",
365-
(
366-
"pyramid_debugtoolbar.panels.performance."
367-
"PerformanceDebugPanel"
368-
),
363+
("pyramid_debugtoolbar.panels.performance.PerformanceDebugPanel"),
369364
"pyramid_debugtoolbar.panels.routes.RoutesDebugPanel",
370365
"pyramid_debugtoolbar.panels.sqla.SQLADebugPanel",
371366
"pyramid_debugtoolbar.panels.tweens.TweensDebugPanel",

warehouse/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,9 @@ def configure(settings=None):
349349
maybe_set(settings, "warehouse.domain", "WAREHOUSE_DOMAIN")
350350
maybe_set(settings, "forklift.domain", "FORKLIFT_DOMAIN")
351351
maybe_set(settings, "auth.domain", "AUTH_DOMAIN")
352+
maybe_set(
353+
settings, "userdocs.domain", "USERDOCS_DOMAIN", default="https://docs.pypi.org"
354+
)
352355
maybe_set(settings, "warehouse.legacy_domain", "WAREHOUSE_LEGACY_DOMAIN")
353356
maybe_set(settings, "site.name", "SITE_NAME", default="Warehouse")
354357
maybe_set(settings, "aws.key_id", "AWS_ACCESS_KEY_ID")

0 commit comments

Comments
 (0)