Skip to content

Commit 521f357

Browse files
authored
Merge pull request khornberg#96 from khornberg/feature/api-gateway-uploads
Version: 3.1.0 -> 3.2.0
2 parents 1515303 + 4839058 commit 521f357

File tree

7 files changed

+88
-22
lines changed

7 files changed

+88
-22
lines changed

docs/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ <h2 id="configuration">Configuration</h2>
103103
TABLE: "elasticpypi" # You can change me if you want, but do you?
104104
USERNAME: "elasticpypi" # You can change me if you want, but do you?
105105
PASSWORD: "something-secretive" # CHANGE ME
106+
OVERWRITE: false # Allow uploads to overwrite already existing packages
106107
</code></pre><p>
107108

108109
<h2 id="install_dependencies">Install dependencies</h2>

elasticpypi/api.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,41 @@
22
from flask import Flask
33
from flask import send_file
44
from flask import render_template
5+
from flask import request
6+
from flask import abort
57
from elasticpypi.auth import requires_auth
68
from elasticpypi import s3, dynamodb
79
from elasticpypi.config import config
810

911
app = Flask(__name__)
1012

1113

12-
@app.route("/simple/")
14+
@app.route('/simple/', methods=['GET', 'POST'])
1315
@requires_auth
1416
def simple():
17+
if request.method == 'POST':
18+
f = request.files['content']
19+
if '/' in f.filename:
20+
abort(400)
21+
print(config)
22+
if config['overwrite'] == 'false' and s3.exists(f.filename):
23+
abort(409)
24+
s3.upload(f.filename, f.stream)
25+
return '', 200
1526
db = boto3.resource('dynamodb')
1627
prefixes = dynamodb.list_packages(db)
1728
return render_template('simple.html', prefixes=prefixes, stage=config['stage'])
1829

1930

20-
@app.route("/simple/<name>/")
31+
@app.route('/simple/<name>/')
2132
@requires_auth
2233
def simple_name(name):
2334
db = boto3.resource('dynamodb')
2435
packages = dynamodb.list_packages_by_name(db, name)
2536
return render_template('links.html', packages=packages, package=name, stage=config['stage'])
2637

2738

28-
@app.route("/packages/<name>")
39+
@app.route('/packages/<name>')
2940
@requires_auth
3041
def packages_name(name):
3142
fp = s3.download(name)

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"name": "elasticpypi",
99
"description": "elastic pypi",
10-
"version": "3.1.0",
10+
"version": "3.2.0",
1111
"main": "index.js",
1212
"directories": {
1313
"test": "tests"

pytest.ini

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ env =
77
TABLE=elasticpypi
88
USERNAME=elasticpypi
99
PASSWORD=something-secretive
10+
OVERWRITE=false

readme.md

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
elastic pypi
22
------------
33

4-
A *mostly* functional simple pypi service running on AWS.
4+
A fully functional, self-hosted simple pypi service running on AWS.
55

66
# Caveats
77

8-
**`python setup.py sdist` does not work**
9-
10-
This is a limitation of AWS API Gateway. There are numerous other ways to upload your packages to the S3 bucket.
11-
128
**Cannot browse with a browser**
139

1410
This is a limitation of current browsers. They have removed basic authentication for remote urls via the url (e.g. x:y@z). `WWW-Authenticate` responses do not work with AWS Lambda.
@@ -39,6 +35,7 @@ provider:
3935
TABLE: "elasticpypi" # You can change me if you want, but do you?
4036
USERNAME: "elasticpypi" # You can change me if you want, but do you?
4137
PASSWORD: "something-secretive" # CHANGE ME
38+
OVERWRITE: false # Allow uploads to overwrite already existing packages
4239
```
4340

4441
# Deploy
@@ -85,6 +82,8 @@ The example below runs the full test suite. To debug, add `/bin/bash` to the end
8582

8683
# Changelog
8784

85+
* 2017-12-27 Uploads work. Manually tested with `python setup.py upload` and `twine upload`
86+
8887
* *2017-12-22* Use Python 3, downloads go through the API Gateway so pip's caching now works
8988

9089
* *2017-03-24* The configuration has moved from `./elasticpypi/config.json` to `./serverless.yml` and is consumed by elasticpypi as environment variables. If you are upgrading from an older version, you may need to migrate your configuration to serverless.yml.

serverless.yml

+20-13
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,17 @@ provider:
55
runtime: python3.6
66
memorySize: 128
77
stage: dev
8-
# profile: "some-local-aws-config-profile"
8+
# profile: 'some-local-aws-config-profile'
99
# region: us-east-1
1010

1111
environment:
1212
SERVICE: ${self:service}
13-
STAGE: "/${self:provider.stage}"
14-
BUCKET: "elasticpypi" # CHANGE ME
15-
TABLE: "elasticpypi"
16-
USERNAME: "elasticpypi"
17-
PASSWORD: "something-secretive" # CHANGE ME
13+
STAGE: '/${self:provider.stage}'
14+
BUCKET: 'elasticpypi' # CHANGE ME
15+
TABLE: 'elasticpypi'
16+
USERNAME: 'elasticpypi'
17+
PASSWORD: 'something-secretive' # CHANGE ME
18+
OVERWRITE: false
1819

1920
plugins:
2021
- serverless-wsgi
@@ -79,27 +80,27 @@ resources:
7980
TableName: ${self:provider.environment.TABLE}
8081
AttributeDefinitions:
8182
-
82-
AttributeName: "package_name"
83+
AttributeName: 'package_name'
8384
AttributeType: S
8485
-
85-
AttributeName: "version"
86+
AttributeName: 'version'
8687
AttributeType: S
8788
-
88-
AttributeName: "normalized_name"
89+
AttributeName: 'normalized_name'
8990
AttributeType: S
9091
KeySchema:
9192
-
92-
AttributeName: "package_name"
93+
AttributeName: 'package_name'
9394
KeyType: HASH
9495
-
95-
AttributeName: "version"
96+
AttributeName: 'version'
9697
KeyType: RANGE
9798
GlobalSecondaryIndexes:
9899
-
99-
IndexName: "normalized_name-index"
100+
IndexName: 'normalized_name-index'
100101
KeySchema:
101102
-
102-
AttributeName: "normalized_name"
103+
AttributeName: 'normalized_name'
103104
KeyType: HASH
104105
Projection:
105106
ProjectionType: ALL
@@ -139,5 +140,11 @@ resources:
139140
- Effect: Allow
140141
Action: s3:GetObject
141142
Resource: arn:aws:s3:::${self:provider.environment.BUCKET}/*
143+
- Effect: Allow
144+
Action: s3:ListBucket
145+
Resource: arn:aws:s3:::${self:provider.environment.BUCKET}
146+
- Effect: Allow
147+
Action: s3:PutObject
148+
Resource: arn:aws:s3:::${self:provider.environment.BUCKET}/*
142149
Roles:
143150
- Ref: IamRoleLambdaExecution

tests/test_simple.py

+47
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import io
2+
from unittest import mock
13
from flask_testing import TestCase
24
from base64 import b64encode
35
from elasticpypi.api import app
@@ -49,3 +51,48 @@ def test_get_simple_200_from_dynamodb(self):
4951
response = self.client.get('/simple/', headers=self.headers)
5052
self.assert200(response)
5153
self.assertEqual(response.data.decode(), fixtures.simple_html)
54+
55+
def test_post_simple_401(self):
56+
response = self.client.post('/simple/')
57+
self.assert401(response)
58+
59+
@mock.patch('elasticpypi.s3.exists')
60+
@mock.patch('elasticpypi.s3.upload')
61+
def test_post_simple_200(self, upload, exists):
62+
exists.return_value = False
63+
f = io.BytesIO('hello'.encode('utf-8'))
64+
response = self.client.post('/simple/', headers=self.headers, data={'content': (f, 'py-0.1.2.tar.gz')})
65+
self.assertStatus(response, 200)
66+
upload.assert_called_with('py-0.1.2.tar.gz', mock.ANY)
67+
f.close()
68+
69+
@mock.patch('elasticpypi.s3.upload')
70+
def test_cannot_post_file_with_slash_in_the_file_name(self, upload):
71+
f = io.BytesIO('hello'.encode('utf-8'))
72+
response = self.client.post('/simple/', headers=self.headers, data={'content': (f, '../py-0.1.2.tar.gz')})
73+
self.assert400(response)
74+
assert not upload.called
75+
f.close()
76+
77+
@mock.patch('elasticpypi.s3.list_packages')
78+
@mock.patch('elasticpypi.s3.upload')
79+
def test_cannot_post_file_when_package_already_exists_and_overwrite_is_false(self, upload, list_packages):
80+
list_packages.return_value = ['py-0.1.2.tar.gz']
81+
f = io.BytesIO('hello'.encode('utf-8'))
82+
response = self.client.post('/simple/', headers=self.headers, data={'content': (f, 'py-0.1.2.tar.gz')})
83+
self.assertEqual(response.status_code, 409)
84+
list_packages.assert_called_with('py-0.1.2.tar.gz', True)
85+
assert not upload.called
86+
f.close()
87+
88+
@mock.patch('elasticpypi.s3.exists')
89+
@mock.patch('elasticpypi.s3.upload')
90+
@mock.patch('elasticpypi.api.config')
91+
def test_can_post_file_when_package_exists_and_overwrite_is_true(self, config, upload, exists):
92+
config.return_value = {'OVERWRITE': 'true'}
93+
exists.return_value = True
94+
f = io.BytesIO('hello'.encode('utf-8'))
95+
response = self.client.post('/simple/', headers=self.headers, data={'content': (f, 'py-0.1.2.tar.gz')})
96+
self.assertStatus(response, 200)
97+
upload.assert_called_with('py-0.1.2.tar.gz', mock.ANY)
98+
f.close()

0 commit comments

Comments
 (0)