x2s3 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
x2s3-0.8.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2024, Howard Hughes Medical Institute
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
x2s3-0.8.0/PKG-INFO ADDED
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: x2s3
3
+ Version: 0.8.0
4
+ Summary: RESTful web service which makes any storage system X available as an S3-compatible REST API
5
+ Author-email: Konrad Rokicki <rokicki@janelia.hhmi.org>
6
+ License: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/JaneliaSciComp/x2s3
8
+ Project-URL: Repository, https://github.com/JaneliaSciComp/x2s3
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: License :: OSI Approved :: BSD License
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.6
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: aiobotocore
16
+ Requires-Dist: boto3
17
+ Requires-Dist: botocore
18
+ Requires-Dist: fastapi
19
+ Requires-Dist: loguru
20
+ Requires-Dist: pydantic
21
+ Requires-Dist: pydantic-settings
22
+ Requires-Dist: python-dateutil
23
+ Requires-Dist: pytest
24
+ Requires-Dist: starlette
25
+ Requires-Dist: typing_extensions
26
+ Requires-Dist: uvicorn
27
+ Dynamic: license-file
28
+
29
+ # x2s3
30
+
31
+ ![Python CI](https://github.com/JaneliaSciComp/x2s3/actions/workflows/python-ci.yml/badge.svg)
32
+
33
+ RESTful web service which makes any storage system *X* available as an S3-compatible REST API, hence the name "X to S3". It was initially built to support cloud-compatible microscopy image viewers such as [N5 Viewer](https://github.com/saalfeldlab/n5-viewer) (BigDataViewer) and [Neuroglancer](https://github.com/google/neuroglancer).
34
+
35
+ At Janelia, we use **x2s3** to make private buckets on Seagate Lyve appear public, and to proxy internal resources (e.g. VAST S3). It can also be used as a pop-up file service for quickly viewing local images in BigDataViewer or Neuroglancer.
36
+
37
+ <p align="center">
38
+ <img src="https://raw.githubusercontent.com/JaneliaSciComp/x2s3/main/docs/use_cases.png">
39
+ </p>
40
+
41
+ # Features
42
+
43
+ * Extensible support for backend storage systems
44
+ * Optional web-based bucket explorer
45
+ * Hidden buckets
46
+ * Partial buckets (chroot-like prefixes)
47
+ * Non-blocking object streaming
48
+
49
+ Inspired by S3 proxies such as [oxyno-zeta/s3-proxy](https://github.com/oxyno-zeta/s3-proxy) and [pottava/aws-s3-proxy](https://github.com/pottava/aws-s3-proxy), this service goes a step further to implement enough of the [AWS S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_Reference.html) to be useable by AWS clients, such as BigDataViewer. The S3 proxy implements which do this well (e.g. [gaul/s3proxy](https://github.com/gaul/s3proxy)) only proxy a single bucket at a time.
50
+
51
+ S3 endpoints implemented:
52
+ * [GetBucketAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketAcl.html)
53
+ * [GetObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html)
54
+ * [HeadObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html)
55
+ * [ListBuckets](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html)
56
+ * [ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html)
57
+
58
+ S3 features omitted:
59
+ * Permissions
60
+ * Encryption
61
+ * Versioning
62
+ * RequestPayer
63
+ * etc.
64
+
65
+ # Running
66
+
67
+ Create a `config.yaml` file that contains all of the buckets you want to serve. You can get started quickly by using the provided example template:
68
+
69
+ ```bash
70
+ cp config.template.yaml config.yaml
71
+ ```
72
+
73
+ See the [documentation](docs/Config.md) for more information about the configuration file.
74
+
75
+ The simplest way to run the service is to use Docker:
76
+
77
+ ```bash
78
+ docker run -it -p 8000:8000 -v ./config.yaml:/app/x2s3/config.yaml ghcr.io/janeliascicomp/x2s3:latest
79
+ ```
80
+
81
+ You can also run the service with Python/Uvicorn directly. See the [development documentation](docs/Development.md) for more information on setting that up.
82
+
83
+
84
+ ## Production Deployment
85
+
86
+ For production deployments, we recommend using an orchestrator (like Docker Compose) to run the prebuilt Docker container along with an Nginx reverse proxy which provides caching and TLS termination.
87
+
88
+ Create a `./docker/.env` file that looks like this:
89
+
90
+ ```bash
91
+ CONFIG_FILE=/path/to/config.yaml
92
+ VAR_DIR=/path/to/var/dir
93
+ CERT_DIR=/path/to/certs
94
+ NGINX_CACHE_DIR=/path/to/cache
95
+ ```
96
+
97
+ These properties configure the service as follows:
98
+ * `CONFIG_FILE`: path to the `config.yaml` settings file
99
+ * `VAR_DIR`: optional path to the var directory containing access keys referenced by `config.yaml`
100
+ * `CERT_FILE`: optional path to the SSL cert file
101
+ * `KEY_FILE`: optional path to the SSL key file
102
+ * `NGINX_CACHE_DIR`: path for Nginx response caching (you can disable caching by editing `nginx.conf`)
103
+
104
+ Now you can bring up the container:
105
+
106
+ ```bash
107
+ cd docker/
108
+ docker compose up -d
109
+ ```
110
+
111
+ # Additional Documentation
112
+
113
+ * [Configuation](docs/Config.md) - how to use `config.yaml` to configure the service
114
+ * [Development](docs/Development.md) - notes on developing the service codebase
115
+
116
+
117
+ # Attributions
118
+
119
+ * Proxy icons created by [Uniconlabs - Flaticon](https://www.flaticon.com/free-icons/proxy)
120
+ * [AWS S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_Reference.html) Reference
x2s3-0.8.0/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # x2s3
2
+
3
+ ![Python CI](https://github.com/JaneliaSciComp/x2s3/actions/workflows/python-ci.yml/badge.svg)
4
+
5
+ RESTful web service which makes any storage system *X* available as an S3-compatible REST API, hence the name "X to S3". It was initially built to support cloud-compatible microscopy image viewers such as [N5 Viewer](https://github.com/saalfeldlab/n5-viewer) (BigDataViewer) and [Neuroglancer](https://github.com/google/neuroglancer).
6
+
7
+ At Janelia, we use **x2s3** to make private buckets on Seagate Lyve appear public, and to proxy internal resources (e.g. VAST S3). It can also be used as a pop-up file service for quickly viewing local images in BigDataViewer or Neuroglancer.
8
+
9
+ <p align="center">
10
+ <img src="https://raw.githubusercontent.com/JaneliaSciComp/x2s3/main/docs/use_cases.png">
11
+ </p>
12
+
13
+ # Features
14
+
15
+ * Extensible support for backend storage systems
16
+ * Optional web-based bucket explorer
17
+ * Hidden buckets
18
+ * Partial buckets (chroot-like prefixes)
19
+ * Non-blocking object streaming
20
+
21
+ Inspired by S3 proxies such as [oxyno-zeta/s3-proxy](https://github.com/oxyno-zeta/s3-proxy) and [pottava/aws-s3-proxy](https://github.com/pottava/aws-s3-proxy), this service goes a step further to implement enough of the [AWS S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_Reference.html) to be useable by AWS clients, such as BigDataViewer. The S3 proxy implements which do this well (e.g. [gaul/s3proxy](https://github.com/gaul/s3proxy)) only proxy a single bucket at a time.
22
+
23
+ S3 endpoints implemented:
24
+ * [GetBucketAcl](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketAcl.html)
25
+ * [GetObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html)
26
+ * [HeadObject](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html)
27
+ * [ListBuckets](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListBuckets.html)
28
+ * [ListObjectsV2](https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html)
29
+
30
+ S3 features omitted:
31
+ * Permissions
32
+ * Encryption
33
+ * Versioning
34
+ * RequestPayer
35
+ * etc.
36
+
37
+ # Running
38
+
39
+ Create a `config.yaml` file that contains all of the buckets you want to serve. You can get started quickly by using the provided example template:
40
+
41
+ ```bash
42
+ cp config.template.yaml config.yaml
43
+ ```
44
+
45
+ See the [documentation](docs/Config.md) for more information about the configuration file.
46
+
47
+ The simplest way to run the service is to use Docker:
48
+
49
+ ```bash
50
+ docker run -it -p 8000:8000 -v ./config.yaml:/app/x2s3/config.yaml ghcr.io/janeliascicomp/x2s3:latest
51
+ ```
52
+
53
+ You can also run the service with Python/Uvicorn directly. See the [development documentation](docs/Development.md) for more information on setting that up.
54
+
55
+
56
+ ## Production Deployment
57
+
58
+ For production deployments, we recommend using an orchestrator (like Docker Compose) to run the prebuilt Docker container along with an Nginx reverse proxy which provides caching and TLS termination.
59
+
60
+ Create a `./docker/.env` file that looks like this:
61
+
62
+ ```bash
63
+ CONFIG_FILE=/path/to/config.yaml
64
+ VAR_DIR=/path/to/var/dir
65
+ CERT_DIR=/path/to/certs
66
+ NGINX_CACHE_DIR=/path/to/cache
67
+ ```
68
+
69
+ These properties configure the service as follows:
70
+ * `CONFIG_FILE`: path to the `config.yaml` settings file
71
+ * `VAR_DIR`: optional path to the var directory containing access keys referenced by `config.yaml`
72
+ * `CERT_FILE`: optional path to the SSL cert file
73
+ * `KEY_FILE`: optional path to the SSL key file
74
+ * `NGINX_CACHE_DIR`: path for Nginx response caching (you can disable caching by editing `nginx.conf`)
75
+
76
+ Now you can bring up the container:
77
+
78
+ ```bash
79
+ cd docker/
80
+ docker compose up -d
81
+ ```
82
+
83
+ # Additional Documentation
84
+
85
+ * [Configuation](docs/Config.md) - how to use `config.yaml` to configure the service
86
+ * [Development](docs/Development.md) - notes on developing the service codebase
87
+
88
+
89
+ # Attributions
90
+
91
+ * Proxy icons created by [Uniconlabs - Flaticon](https://www.flaticon.com/free-icons/proxy)
92
+ * [AWS S3 API](https://docs.aws.amazon.com/AmazonS3/latest/API/Type_API_Reference.html) Reference
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "x2s3"
7
+ version = "0.8.0"
8
+ description = "RESTful web service which makes any storage system X available as an S3-compatible REST API"
9
+ readme = "README.md"
10
+ authors = [
11
+ { name = "Konrad Rokicki", email = "rokicki@janelia.hhmi.org" }
12
+ ]
13
+ license = { text = "BSD-3-Clause" }
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: BSD License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ requires-python = ">=3.6"
20
+ dependencies = [
21
+ "aiobotocore",
22
+ "boto3",
23
+ "botocore",
24
+ "fastapi",
25
+ "loguru",
26
+ "pydantic",
27
+ "pydantic-settings",
28
+ "python-dateutil",
29
+ "pytest",
30
+ "starlette",
31
+ "typing_extensions",
32
+ "uvicorn"
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/JaneliaSciComp/x2s3"
37
+ Repository = "https://github.com/JaneliaSciComp/x2s3"
38
+
39
+ [tool.setuptools]
40
+ packages = ["x2s3"]
x2s3-0.8.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,297 @@
1
+ import urllib.parse
2
+
3
+ import pytest
4
+ from fastapi.testclient import TestClient
5
+ from pydantic import HttpUrl
6
+
7
+ from xml.etree.ElementTree import Element
8
+ from x2s3.app import create_app
9
+ from x2s3.settings import Target, Settings
10
+ from x2s3.utils import parse_xml
11
+
12
+ obj_path = '/janelia-data-examples/jrc_mus_lung_covid.n5/render/v1_acquire_align___20210609_224836/s0/0/0/0'
13
+
14
+ @pytest.fixture
15
+ def get_settings():
16
+ settings = Settings()
17
+ settings.base_url = HttpUrl('http://testserver')
18
+ settings.virtual_buckets = True
19
+ settings.targets = [
20
+ Target(
21
+ name='janelia-data-examples',
22
+ options={'bucket':'janelia-data-examples'}
23
+ ),
24
+ Target(
25
+ name='with-prefix',
26
+ options={
27
+ 'bucket':'janelia-data-examples',
28
+ 'prefix':'jrc_mus_lung_covid.n5/'
29
+ }
30
+ ),
31
+ Target(
32
+ name='hidden-with-endpoint',
33
+ browseable=False,
34
+ options={
35
+ 'bucket':'janelia-data-examples',
36
+ 'endpoint':'https://s3.amazonaws.com',
37
+ }
38
+ )
39
+ ]
40
+ return settings
41
+
42
+
43
+ @pytest.fixture
44
+ def app(get_settings):
45
+ return create_app(get_settings)
46
+
47
+
48
+ def test_acl(app):
49
+ with TestClient(app) as client:
50
+ response = client.get(f"/janelia-data-examples?acl")
51
+ assert response.status_code == 200
52
+ assert response.headers['content-type'] == "application/xml"
53
+ assert response.text.count("<Grant>") == 1
54
+
55
+
56
+ def test_get_html_root(app):
57
+ with TestClient(app) as client:
58
+ response = client.get("/")
59
+ assert response.status_code == 200
60
+ assert response.headers['content-type'].startswith("text/html")
61
+ for target in app.settings.targets:
62
+ if target.browseable:
63
+ assert target.name in response.text
64
+ else:
65
+ assert target.name not in response.text
66
+
67
+
68
+ def test_get_html_listing(app):
69
+ with TestClient(app) as client:
70
+ response = client.get("/janelia-data-examples/jrc_mus_lung_covid.n5/")
71
+ assert response.status_code == 200
72
+ assert response.headers['content-type'].startswith("text/html")
73
+ assert '<html>' in response.text
74
+ assert '<head>' in response.text
75
+ assert 'attributes.json' in response.text
76
+
77
+
78
+ def test_list_objects(app):
79
+ with TestClient(app) as client:
80
+ bucket_name = 'janelia-data-examples'
81
+ max_keys = 7
82
+ response = client.get(f"/{bucket_name}?list-type=2&max-keys={max_keys}")
83
+ assert response.status_code == 200
84
+ root = parse_xml(response.text)
85
+ assert root.tag == "ListBucketResult"
86
+ assert root.find('Name').text == bucket_name
87
+
88
+ # CommonPrefixes are only returned when there is a delimiter
89
+ assert len(root.findall('CommonPrefixes')) == 0
90
+
91
+ contents = root.findall('Contents')
92
+ assert len(contents) <= max_keys
93
+
94
+ if root.find('NextContinuationToken') is not None:
95
+ assert root.find('IsTruncated').text == "true"
96
+
97
+ assert isinstance(contents[0].find('Key'), Element)
98
+ assert isinstance(contents[0].find('Size'), Element)
99
+ assert isinstance(contents[0].find('LastModified'), Element)
100
+
101
+
102
+ def test_list_objects_delimiter(app):
103
+ with TestClient(app) as client:
104
+ bucket_name = 'janelia-data-examples'
105
+ max_keys = 9
106
+ response = client.get(f"/{bucket_name}?list-type=2&delimiter=/&max-keys={max_keys}")
107
+ assert response.status_code == 200
108
+ root = parse_xml(response.text)
109
+ assert root.tag == "ListBucketResult"
110
+ assert root.find('Name').text == bucket_name
111
+ assert root.find('Delimiter').text == '/'
112
+ assert len(root.findall('CommonPrefixes')) >= 1
113
+ assert root.find('IsTruncated').text == "false"
114
+
115
+
116
+ def test_list_objects_continuation(app):
117
+ with TestClient(app) as client:
118
+ bucket_name = 'janelia-data-examples'
119
+ max_keys = 4
120
+ uri = f"/{bucket_name}?list-type=2&max-keys={max_keys}&prefix=jrc_mus_lung_covid.n5/render/v1_acquire_align___20210609_224836/s0/0/0"
121
+
122
+ token_param = ''
123
+ total = 0
124
+ c = 0
125
+ while True:
126
+ url = f"{uri}{token_param}"
127
+ print(f"Fetching {url}")
128
+ response = client.get(url)
129
+ assert response.status_code == 200
130
+ root = parse_xml(response.text)
131
+
132
+ assert root.tag == "ListBucketResult"
133
+ assert root.find('Name').text == bucket_name
134
+ assert root.find('MaxKeys').text == str(max_keys)
135
+ assert len(root.findall('CommonPrefixes')) == 0
136
+
137
+ contents = root.findall('Contents')
138
+ print(f"Got {len(contents)} results")
139
+ total += len(contents)
140
+ assert len(contents) <= max_keys
141
+
142
+ next_token_elem = root.find('NextContinuationToken')
143
+ if next_token_elem is not None:
144
+ assert root.find('IsTruncated').text == "true"
145
+ else:
146
+ break
147
+
148
+ safe_token = urllib.parse.quote_plus(next_token_elem.text)
149
+ token_param = f"&continuation-token={safe_token}"
150
+
151
+ c += 1
152
+ if c>10:
153
+ assert False
154
+
155
+ assert total == 6
156
+
157
+
158
+ def test_head_object(app):
159
+ with TestClient(app) as client:
160
+ response = client.head("/janelia-data-examples/jrc_mus_lung_covid.n5/attributes.json")
161
+ assert response.status_code == 200
162
+ response = client.head("/janelia-data-examples/jrc_mus_lung_covid.n5/")
163
+ assert response.status_code == 404
164
+
165
+
166
+ def test_prefixed_head_object(app):
167
+ with TestClient(app) as client:
168
+ response = client.head("/with-prefix/attributes.json")
169
+ assert response.status_code == 200
170
+ response = client.head("/with-prefix/render/attributes.json")
171
+ assert response.status_code == 200
172
+ response = client.head("/with-prefix/render/")
173
+ assert response.status_code == 404
174
+
175
+
176
+ def test_get_object(app):
177
+ with TestClient(app) as client:
178
+ response = client.get("/janelia-data-examples/jrc_mus_lung_covid.n5/attributes.json")
179
+ assert response.status_code == 200
180
+ json_obj = response.json()
181
+ assert 'n5' in json_obj
182
+
183
+
184
+ def test_prefixed_get_object(app):
185
+ with TestClient(app) as client:
186
+ response = client.get("/with-prefix/attributes.json")
187
+ assert response.status_code == 200
188
+ json_obj = response.json()
189
+ assert 'n5' in json_obj
190
+
191
+
192
+ def test_virtual_host_get_object(app):
193
+ with TestClient(app) as client:
194
+ response = client.get("/jrc_mus_lung_covid.n5/attributes.json",
195
+ headers={'Host':'janelia-data-examples.testserver'})
196
+ assert response.status_code == 200
197
+ json_obj = response.json()
198
+ assert 'n5' in json_obj
199
+
200
+
201
+ def test_get_object_hidden(app):
202
+ with TestClient(app) as client:
203
+ response = client.get("/hidden-with-endpoint/jrc_mus_lung_covid.n5/attributes.json")
204
+ assert response.status_code == 200
205
+ json_obj = response.json()
206
+ assert 'n5' in json_obj
207
+
208
+
209
+ def test_get_object_range_first(app):
210
+ with TestClient(app) as client:
211
+ # Test a valid range request (first 100 bytes)
212
+ response = client.get(
213
+ obj_path,
214
+ headers={"Range": "bytes=0-99"}
215
+ )
216
+ assert response.status_code == 206 # Partial Content
217
+ assert 'Content-Range' in response.headers
218
+ assert response.headers['Content-Range'] == 'bytes 0-99/987143'
219
+ assert len(response.content) == 100
220
+
221
+ def test_get_object_range_mid(app):
222
+ with TestClient(app) as client:
223
+ # Test a valid range request (bytes 100-199)
224
+ response = client.get(
225
+ obj_path,
226
+ headers={"Range": "bytes=100-199"}
227
+ )
228
+ assert response.status_code == 206 # Partial Content
229
+ assert 'Content-Range' in response.headers
230
+ assert response.headers['Content-Range'] == 'bytes 100-199/987143'
231
+ assert len(response.content) == 100
232
+
233
+ def test_get_object_range_last(app):
234
+ with TestClient(app) as client:
235
+ # Test a valid range request (last 100 bytes)
236
+ response = client.get(
237
+ obj_path,
238
+ headers={"Range": "bytes=-100"}
239
+ )
240
+ assert response.status_code == 206 # Partial Content
241
+ assert 'Content-Range' in response.headers
242
+ assert len(response.content) == 100
243
+
244
+ def test_get_object_range_out_of_bounds(app):
245
+ with TestClient(app) as client:
246
+ # Test invalid range request (out of bounds)
247
+ response = client.get(
248
+ obj_path,
249
+ headers={"Range": "bytes=1000000-2000000"}
250
+ )
251
+ assert response.status_code == 416 # Range Not Satisfiable
252
+ root = parse_xml(response.text)
253
+ assert root.find('Code').text == 'InvalidRange'
254
+
255
+
256
+ def test_prefixed_list_objects(app):
257
+ with TestClient(app) as client:
258
+ bucket_name = 'with-prefix'
259
+ response = client.get(f"/{bucket_name}?list-type=2&delimiter=/")
260
+ assert response.status_code == 200
261
+ root = parse_xml(response.text)
262
+ assert root.tag == "ListBucketResult"
263
+ assert root.find('Name').text == bucket_name
264
+ assert root.find('Delimiter').text == '/'
265
+ assert len(root.findall('CommonPrefixes')) == 2
266
+ assert root.find('IsTruncated').text == "false"
267
+
268
+
269
+ def test_get_object_missing(app):
270
+ with TestClient(app) as client:
271
+ response = client.get("/janelia-data-examples/missing")
272
+ assert response.status_code == 404
273
+ assert response.headers['content-type'] == "application/xml"
274
+
275
+
276
+ def test_bucket_missing(app):
277
+ with TestClient(app) as client:
278
+ response = client.get("/missing/attributes.json")
279
+ assert response.status_code == 404
280
+ assert response.headers['content-type'] == "application/xml"
281
+ root = parse_xml(response.text)
282
+ assert root.find('Code').text == 'NoSuchBucket'
283
+
284
+
285
+ def test_list_objects_error(app):
286
+ with TestClient(app) as client:
287
+ response = client.get(f"/janelia-data-examples?list-type=2&max-keys=aaa")
288
+ assert response.status_code == 400
289
+ assert response.headers['content-type'] == "application/json"
290
+
291
+
292
+ def test_get_object_precedence(app):
293
+ with TestClient(app) as client:
294
+ response = client.get(f"/janelia-data-examples/jrc_mus_lung_covid.n5/attributes.json?list-type=2")
295
+ assert response.status_code == 200
296
+ json_obj = response.json()
297
+ assert 'n5' in json_obj
@@ -0,0 +1,41 @@
1
+ import urllib.parse
2
+
3
+ import pytest
4
+ from fastapi.testclient import TestClient
5
+ from pydantic import HttpUrl
6
+
7
+ from xml.etree.ElementTree import Element
8
+ from x2s3.app import create_app
9
+ from x2s3.settings import Target, Settings
10
+ from x2s3.utils import parse_xml
11
+
12
+
13
+ @pytest.fixture
14
+ def get_settings():
15
+ settings = Settings()
16
+ settings.targets = [
17
+ ]
18
+ return settings
19
+
20
+
21
+ @pytest.fixture
22
+ def app(get_settings):
23
+ return create_app(get_settings)
24
+
25
+
26
+ @pytest.fixture
27
+ def client(app):
28
+ return TestClient(app)
29
+
30
+
31
+ def test_get_favicon(client):
32
+ response = client.get("/favicon.ico")
33
+ assert response.status_code == 200
34
+ assert response.headers['content-type'].startswith("image")
35
+
36
+
37
+ def test_get_robotstxt(client):
38
+ response = client.get("/robots.txt")
39
+ assert response.status_code == 200
40
+ assert response.headers['content-type'].startswith("text/plain")
41
+ assert response.text == "User-agent: *\nDisallow: /"