-
Notifications
You must be signed in to change notification settings - Fork 32
Conversion to poetry project, added support for multiple ranges. #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nparker2020
wants to merge
12
commits into
danvk:master
Choose a base branch
from
nparker2020:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
43cb82c
Code reorg to poetry-ify this project.
nparker2020 0a7f55b
Tests pass and responses from server with multiple ranges *seem* legi…
nparker2020 026e049
Debugged multipart responses, they now get validated in a unit test.
nparker2020 d8eb054
Fixed bug with payload length when serving something that is not of C…
nparker2020 c93508c
Cleaned up some comments and set version number to be newer than what…
nparker2020 dbcf561
Renamed package dir to reflect package name in pypi.
nparker2020 6bd7240
Updating READMEs
nparker2020 acc991e
Merge pull request #1 from nparker2020/refactor-and-multi-range-support
nparker2020 73bc36d
Forgot to hit save on the server script
nparker2020 169dda9
Merge pull request #2 from nparker2020/refactor-and-multi-range-support
nparker2020 411888c
Geez, forgetting to same everything.
nparker2020 dd75931
Merge pull request #3 from nparker2020/refactor-and-multi-range-support
nparker2020 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| [tool.poetry] | ||
| name = "rangehttpserver" | ||
| version = "1.4.0" | ||
| description = "" | ||
| authors = ["Noah Parker <nparker@cyvl.ai>"] | ||
| readme = "README.md" | ||
|
|
||
| [tool.poetry.dependencies] | ||
| python = "^3.8" | ||
| pytest = "^7.4.2" | ||
| pytest-cov = "^4.1.0" | ||
| coveralls = "^3.3.1" | ||
| requests-toolbelt = "^1.0.0" | ||
|
|
||
|
|
||
| [tool.poetry.group.dev.dependencies] | ||
| black = { extras = ["jupyter"], version = "^23.1.0" } | ||
| mypy = "^1.1.1" | ||
| pytest = "^7.2.2" | ||
| pylint = "^2.17.0" | ||
| boto3-stubs = { extras = ["codepipeline"], version = "^1.26.89" } | ||
| poethepoet = "^0.24.1" | ||
|
|
||
| [tool.poe.tasks] | ||
| # Or `pylint mypackage` | ||
| lint = 'pylint --recursive=y --fail-under 5 .' | ||
| format = 'black .' | ||
| check-formatting = 'black --check .' | ||
| check-types = 'mypy -p cyvl_data_fusion -p tests' | ||
| unit-tests = 'pytest --cov' | ||
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| from rangehttpserver.servers.server import start | ||
|
|
||
| if __name__ == '__main__': | ||
| start() |
162 changes: 162 additions & 0 deletions
162
rangehttpserver/request_handlers/range_request_handler.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| import re | ||
|
|
||
|
|
||
| try: | ||
| # Python3 | ||
| from http.server import SimpleHTTPRequestHandler | ||
|
|
||
| except ImportError: | ||
| # Python 2 | ||
| from SimpleHTTPServer import SimpleHTTPRequestHandler | ||
|
|
||
| import os | ||
|
|
||
| MULTIPART_BOUNDARY_STRING = "python-boundary-string-1234" | ||
| class RangeRequestHandler(SimpleHTTPRequestHandler): | ||
| """Adds support for HTTP 'Range' requests to SimpleHTTPRequestHandler | ||
|
|
||
| The approach is to: | ||
| - Override send_head to look for 'Range' and respond appropriately. | ||
| - Override copyfile to only transmit a range when requested. | ||
| """ | ||
| def send_head(self): | ||
| if 'Range' not in self.headers: | ||
| self.ranges = None | ||
| return SimpleHTTPRequestHandler.send_head(self) | ||
| try: | ||
| self.ranges = parse_byte_range(self.headers['Range']) | ||
| except ValueError as e: | ||
| self.send_error(400, 'Invalid byte range') | ||
| return None | ||
|
|
||
| # Mirroring SimpleHTTPServer.py here | ||
| path = self.translate_path(self.path) | ||
| f = None | ||
| ctype = self.guess_type(path) | ||
| self.ctype = ctype | ||
| try: | ||
| f = open(path, 'rb') | ||
| except IOError: | ||
| self.send_error(404, 'File not found') | ||
| return None | ||
|
|
||
| response_length = 0 | ||
|
|
||
| sent_response = False | ||
|
|
||
| for range in self.ranges: | ||
|
|
||
| first, last = range | ||
|
|
||
| fs = os.fstat(f.fileno()) | ||
| file_len = fs[6] | ||
| if first >= file_len: | ||
| self.send_error(416, 'Requested Range Not Satisfiable') | ||
| return None | ||
| if last is None or last >= file_len: | ||
| last = file_len - 1 | ||
|
|
||
| value = 'bytes %s-%s/%s' % (first, last, file_len) | ||
|
|
||
| message_length = last - first + 1 | ||
|
|
||
| if sent_response is False: | ||
| self.send_response(206) | ||
| if len(self.ranges) == 1: | ||
| self.send_header('Content-type', ctype) | ||
| else: | ||
| self.send_header('Content-type', 'multipart/byteranges; boundary='+MULTIPART_BOUNDARY_STRING) | ||
| sent_response = True | ||
|
|
||
| if len(self.ranges) > 1: | ||
| message_length += len( self.get_section_boundary_string(ctype, first, last, file_len) ) | ||
|
|
||
| response_length += message_length | ||
|
|
||
| self.send_header('Content-Range', | ||
| value) | ||
|
|
||
| if len(self.ranges) > 1: | ||
| response_length += len(("\r\n--"+MULTIPART_BOUNDARY_STRING+"--").encode()) | ||
|
|
||
| self.send_header('Content-Length', str(response_length)) | ||
| self.send_header('Last-Modified', self.date_time_string(fs.st_mtime)) | ||
| self.end_headers() | ||
| return f | ||
|
|
||
| def end_headers(self): | ||
| self.send_header('Accept-Ranges', 'bytes') | ||
| return SimpleHTTPRequestHandler.end_headers(self) | ||
|
|
||
| def copyfile(self, source, outputfile): | ||
| if not self.ranges: | ||
| return SimpleHTTPRequestHandler.copyfile(self, source, outputfile) | ||
|
|
||
| # SimpleHTTPRequestHandler uses shutil.copyfileobj, which doesn't let | ||
| # you stop the copying before the end of the file. | ||
| #start, stop = self.range # set in send_head() | ||
| #get total length from source | ||
|
|
||
| total_byte_length = os.fstat(source.fileno())[6] | ||
| for range in self.ranges: | ||
| start, stop = range | ||
|
|
||
| if len(self.ranges) > 1: | ||
| boundary_string = self.get_section_boundary_string(self.ctype, start, stop, total_byte_length) | ||
| outputfile.write(boundary_string.encode()) | ||
|
|
||
| copy_byte_range(source, outputfile, start, stop) | ||
|
|
||
| if len(self.ranges) > 1: | ||
| #copy in final boundary string | ||
| outputfile.write(("\r\n--"+MULTIPART_BOUNDARY_STRING+"--").encode()) | ||
|
|
||
| def get_section_boundary_string(self, content_type, start, end, total): | ||
| boundary_string = MULTIPART_BOUNDARY_STRING | ||
| formatted_boundary_string = "\r\n--"+boundary_string+"\r\n" | ||
| content_header = f"Content-Type: {content_type}\r\n" | ||
| range_header = "Content-Range: bytes "+str(start)+"-"+str(end)+"/"+str(total)+"\r\n\r\n" | ||
| return formatted_boundary_string+content_header+range_header | ||
|
|
||
| def copy_byte_range(infile, outfile, start=None, stop=None, bufsize=16*1024): | ||
| """Like shutil.copyfileobj, but only copy a range of the streams. | ||
|
|
||
| Both start and stop are inclusive. | ||
| """ | ||
| if start is not None: infile.seek(start) | ||
| while 1: | ||
| to_read = min(bufsize, stop + 1 - infile.tell() if stop else bufsize) | ||
| buf = infile.read(to_read) | ||
| if not buf: | ||
| break | ||
| outfile.write(buf) | ||
|
|
||
|
|
||
| BYTE_RANGE_RE = re.compile(r'bytes=(\d+)-(\d+)?$') | ||
| def parse_byte_range(byte_range): | ||
| """Returns the two numbers in 'bytes=123-456' or throws ValueError. | ||
|
|
||
| The last number or both numbers may be None. | ||
| """ | ||
| if byte_range.strip() == '': | ||
| return [(None, None)] | ||
|
|
||
| #get index of 'bytes=' str | ||
| start = byte_range.index('bytes=') | ||
| range_data = byte_range[start+6:] | ||
| print(range_data) | ||
| range_list = range_data.split(",") | ||
|
|
||
| ranges = [] | ||
| for range in range_list: | ||
| if len(range.split("-")) < 2: | ||
| raise ValueError('Invalid byte range %s' % byte_range) | ||
| first, last = [x and int(x) for x in range.split("-")] | ||
| if last == '': | ||
| last = None | ||
| if last and last < first: | ||
| raise ValueError('Invalid byte range %s' % byte_range) | ||
|
|
||
| ranges.append((first, last)) | ||
|
|
||
| return ranges |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you unpin the upper bound on these dependencies? Although this is more of an "application" than a "library", it's still likely to be used as a library. As such, we want to avoid pinning downstream users without good reason c.f. https://iscinumpy.dev/post/poetry-versions/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, are any of these actually needed for runtime besides the Python version?