diff --git a/.travis.yml b/.travis.yml index 89e71b1..f57c1e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,9 @@ language: python python: - "2.6" - "2.7" + - "3.3" + - "pypy" install: pip install -e . # command to run tests -script: python test/test_zencoder.py \ No newline at end of file +script: nosetests + diff --git a/README.md b/README.md index 053f68f..b714ca1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # Zencoder +[![Build Status](https://secure.travis-ci.org/schworer/zencoder-py.png)](http://travis-ci.org/schworer/zencoder-py) A Python module for the [Zencoder](http://zencoder.com) API. ## Installation Install from PyPI using `easy_install` or `pip`. + pip install zencoder + ## Dependencies -`zencoder-py` depends on [httplib2](http://code.google.com/p/httplib2/), and uses the `json` or `simplejson` module. +`zencoder-py` depends on [requests](http://python-requests.org), and uses the `json` or `simplejson` module. ## Usage @@ -60,5 +63,4 @@ Docs are in progress, and hosted at Read the Docs: http://zencoder.rtfd.org * [Josh Kennedy](http://github.com/kennedyj) * [Issac Kelly](http://github.com/issackelly) -[![Build Status](https://secure.travis-ci.org/schworer/zencoder-py.png)](http://travis-ci.org/schworer/zencoder-py) diff --git a/setup.py b/setup.py index 0d23c91..1e95e49 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,7 @@ - -from distutils.core import setup +try: + from setuptools import setup +except ImportError: + from distutils.core import setup setup(name='zencoder', version='0.5.2', @@ -8,7 +10,19 @@ author_email='alex.schworer@gmail.com', url='http://github.com/schworer/zencoder-py', license="MIT License", - install_requires=['httplib2'], - packages=['zencoder'] + install_requires=['requests>=1.0'], + tests_require=['mock', 'nose'], + packages=['zencoder'], + platforms='any', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Topic :: Software Development :: Libraries :: Python Modules' + ] ) diff --git a/test/fixtures/account_create.json b/test/fixtures/account_create.json new file mode 100644 index 0000000..657131f --- /dev/null +++ b/test/fixtures/account_create.json @@ -0,0 +1,4 @@ +{ + "api_key": "abcd1234", + "password": "foo" +} diff --git a/test/fixtures/account_details.json b/test/fixtures/account_details.json new file mode 100644 index 0000000..72d230c --- /dev/null +++ b/test/fixtures/account_details.json @@ -0,0 +1,8 @@ +{ + "account_state": "active", + "plan": "Growth", + "minutes_used": 12549, + "minutes_included": 25000, + "billing_state": "active", + "integration_mode":true +} \ No newline at end of file diff --git a/test/fixtures/job_create.json b/test/fixtures/job_create.json new file mode 100644 index 0000000..db41f7d --- /dev/null +++ b/test/fixtures/job_create.json @@ -0,0 +1,10 @@ + { + "outputs": [ + { + "label": null, + "url": "https://zencoder-temp-storage-us-east-1.s3.amazonaws.com/o/20130505/7a9f3b6947c27305079fb105dbfc529e/34356e4d54f0c8fb9c3273203937e795.mp4?AWSAccessKeyId=AKIAI456JQ76GBU7FECA&Signature=Tp9WVinpXKE%2FPrP2M08r54U4EQ0%3D&Expires=1367817210", + "id": 93461812 + } + ], + "id": 45492475 +} diff --git a/test/fixtures/job_create_live.json b/test/fixtures/job_create_live.json new file mode 100644 index 0000000..56a57e0 --- /dev/null +++ b/test/fixtures/job_create_live.json @@ -0,0 +1,12 @@ +{ + "stream_url": "rtmp://foo:1935/live", + "stream_name": "bar", + "outputs": [ + { + "label": null, + "url": "https://zencoder-temp-storage-us-east-1.s3.amazonaws.com", + "id": 97931084 + } + ], + "id": 47010105 +} diff --git a/test/fixtures/job_details.json b/test/fixtures/job_details.json new file mode 100644 index 0000000..6b25bd7 --- /dev/null +++ b/test/fixtures/job_details.json @@ -0,0 +1,69 @@ + { + "job": { + "submitted_at": "2013-05-04T21:36:39-07:00", + "state": "finished", + "privacy": false, + "input_media_file": { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 50, + "state": "finished", + "format": "mpeg4", + "audio_sample_rate": 44100, + "privacy": false, + "height": 720, + "error_message": null, + "url": "s3://test-bucket/test.mov", + "video_bitrate_in_kbps": 1402, + "md5_checksum": null, + "duration_in_ms": 5067, + "test": false, + "id": 45469002, + "finished_at": "2013-05-04T21:36:46-07:00", + "updated_at": "2013-05-04T21:37:12-07:00", + "created_at": "2013-05-04T21:36:39-07:00", + "total_bitrate_in_kbps": 1452, + "width": 1280, + "error_class": null, + "file_size_bytes": 922620 + }, + "test": false, + "id": 45491013, + "finished_at": "2013-05-04T21:37:12-07:00", + "updated_at": "2013-05-04T21:37:12-07:00", + "created_at": "2013-05-04T21:36:39-07:00", + "thumbnails": [], + "output_media_files": [ + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 90, + "state": "finished", + "format": "mpeg4", + "audio_sample_rate": 44100, + "label": null, + "privacy": false, + "height": 720, + "error_message": null, + "url": "https://zencoder-temp-storage-us-east-1.s3.amazonaws.com/o/20130505/fc7f7df4f3eacd6fe4ee88cab28732de/dfc2f1b4eb49ea9ab914c84de6d392fb.mp4?AWSAccessKeyId=AKIAI456JQ76GBU7FECA&Signature=lAc18iXd4ta1Ct0JyazKwYSwdOk%3D&Expires=1367815032", + "video_bitrate_in_kbps": 1440, + "md5_checksum": null, + "duration_in_ms": 5130, + "test": false, + "id": 93457943, + "finished_at": "2013-05-04T21:37:12-07:00", + "updated_at": "2013-05-04T21:37:12-07:00", + "created_at": "2013-05-04T21:36:39-07:00", + "total_bitrate_in_kbps": 1530, + "width": 1280, + "error_class": null, + "file_size_bytes": 973430 + } + ], + "pass_through": null + } +} diff --git a/test/fixtures/job_list.json b/test/fixtures/job_list.json new file mode 100644 index 0000000..47aedf1 --- /dev/null +++ b/test/fixtures/job_list.json @@ -0,0 +1,387 @@ +[ + { + "job": { + "submitted_at": "2013-05-05T01:30:15-05:00", + "state": "finished", + "privacy": false, + "stream": { + "state": "finished", + "height": 720, + "url": "rtmp://live40.us-va.zencoder.io:1935/live", + "duration": 13.3956291675568, + "name": "7177a51b45ccb2b594f890f99fef1fdc", + "test": false, + "id": 22915, + "finished_at": "2013-05-05T01:34:26-05:00", + "updated_at": "2013-05-05T01:35:14-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 1024, + "region": "us-virgina", + "width": 1280, + "protocol": "rtmp" + }, + "input_media_file": { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 131, + "state": "finished", + "format": "flash video", + "audio_sample_rate": 44100, + "privacy": false, + "height": 720, + "error_message": null, + "url": "rtmp://live40.us-va.zencoder.io:1935/live/republish/7177a51b45ccb2b594f890f99fef1fdc", + "video_bitrate_in_kbps": 1228, + "md5_checksum": null, + "duration_in_ms": 11090, + "test": false, + "id": 45472922, + "finished_at": "2013-05-05T01:34:32-05:00", + "updated_at": "2013-05-05T01:34:42-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 1359, + "width": 1280, + "error_class": null, + "file_size_bytes": 304313 + }, + "test": false, + "id": 45494934, + "finished_at": "2013-05-05T01:34:42-05:00", + "updated_at": "2013-05-05T01:34:42-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "thumbnails": [], + "output_media_files": [ + { + "video_codec": null, + "frame_rate": null, + "channels": null, + "audio_codec": null, + "audio_bitrate_in_kbps": null, + "state": "finished", + "format": null, + "audio_sample_rate": null, + "label": "hls_master", + "privacy": false, + "height": null, + "error_message": null, + "url": "http://hls.live.zencdn.net/hls/live/207996/77507/58d23f338d42277bbd74c6281627cea7/master.m3u8", + "video_bitrate_in_kbps": null, + "md5_checksum": null, + "duration_in_ms": null, + "test": false, + "id": 93468543, + "finished_at": "2013-05-05T01:34:35-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": null, + "width": null, + "error_class": null, + "file_size_bytes": 199 + }, + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 128, + "state": "finished", + "format": "mpeg-ts", + "audio_sample_rate": 44100, + "label": "hls_600", + "privacy": false, + "height": 360, + "error_message": null, + "url": "http://hls.live.zencdn.net/hls/live/207996/77507/58d23f338d42277bbd74c6281627cea7/600/index.m3u8", + "video_bitrate_in_kbps": 764, + "md5_checksum": null, + "duration_in_ms": 11100, + "test": false, + "id": 93468540, + "finished_at": "2013-05-05T01:34:39-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 892, + "width": 640, + "error_class": null, + "file_size_bytes": 1217265 + }, + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 128, + "state": "finished", + "format": "mpeg-ts", + "audio_sample_rate": 44100, + "label": "hls_300", + "privacy": false, + "height": 270, + "error_message": null, + "url": "http://hls.live.zencdn.net/hls/live/207996/77507/58d23f338d42277bbd74c6281627cea7/300/index.m3u8", + "video_bitrate_in_kbps": 400, + "md5_checksum": null, + "duration_in_ms": 11100, + "test": false, + "id": 93468539, + "finished_at": "2013-05-05T01:34:40-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 528, + "width": 480, + "error_class": null, + "file_size_bytes": 708537 + }, + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 128, + "state": "finished", + "format": "mpeg-ts", + "audio_sample_rate": 44100, + "label": "hls_1200", + "privacy": false, + "height": 720, + "error_message": null, + "url": "http://hls.live.zencdn.net/hls/live/207996/77507/58d23f338d42277bbd74c6281627cea7/1200/index.m3u8", + "video_bitrate_in_kbps": 1484, + "md5_checksum": null, + "duration_in_ms": 11100, + "test": false, + "id": 93468542, + "finished_at": "2013-05-05T01:34:40-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 1612, + "width": 1280, + "error_class": null, + "file_size_bytes": 2223629 + }, + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 124, + "state": "finished", + "format": "flash video", + "audio_sample_rate": 44100, + "label": "rtmp_300", + "privacy": false, + "height": 270, + "error_message": null, + "url": "rtmp://rtmp.live.zencdn.net/live/15ec8791b4d951a6053de2799170ec93_77507_300@107413", + "video_bitrate_in_kbps": 343, + "md5_checksum": null, + "duration_in_ms": 11160, + "test": false, + "id": 93468534, + "finished_at": "2013-05-05T01:34:41-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 467, + "width": 480, + "error_class": null, + "file_size_bytes": 667832 + }, + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 124, + "state": "finished", + "format": "flash video", + "audio_sample_rate": 44100, + "label": "rtmp_600", + "privacy": false, + "height": 360, + "error_message": null, + "url": "rtmp://rtmp.live.zencdn.net/live/cd5766181ed19b81195db56092b4e500_77507_600@107415", + "video_bitrate_in_kbps": 690, + "md5_checksum": null, + "duration_in_ms": 11160, + "test": false, + "id": 93468535, + "finished_at": "2013-05-05T01:34:41-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 814, + "width": 640, + "error_class": null, + "file_size_bytes": 1152631 + }, + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 124, + "state": "finished", + "format": "flash video", + "audio_sample_rate": 44100, + "label": "rtmp_1200", + "privacy": false, + "height": 720, + "error_message": null, + "url": "rtmp://rtmp.live.zencdn.net/live/a83a306ea10a266e027c3186bff701b3_77507_1200@107417", + "video_bitrate_in_kbps": 1352, + "md5_checksum": null, + "duration_in_ms": 11160, + "test": false, + "id": 93468537, + "finished_at": "2013-05-05T01:34:42-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 1476, + "width": 1280, + "error_class": null, + "file_size_bytes": 2076855 + } + ], + "pass_through": null + } + }, + { + "job": { + "submitted_at": "2013-05-05T01:19:53-05:00", + "state": "finished", + "privacy": false, + "input_media_file": { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 50, + "state": "finished", + "format": "mpeg4", + "audio_sample_rate": 44100, + "privacy": false, + "height": 720, + "error_message": null, + "url": "http://s3.amazonaws.com/zencodertesting/test.mov", + "video_bitrate_in_kbps": 1402, + "md5_checksum": null, + "duration_in_ms": 5067, + "test": false, + "id": 45472534, + "finished_at": "2013-05-05T01:20:02-05:00", + "updated_at": "2013-05-05T01:21:14-05:00", + "created_at": "2013-05-05T01:19:54-05:00", + "total_bitrate_in_kbps": 1452, + "width": 1280, + "error_class": null, + "file_size_bytes": 922620 + }, + "test": false, + "id": 45494545, + "finished_at": "2013-05-05T01:21:14-05:00", + "updated_at": "2013-05-05T01:21:14-05:00", + "created_at": "2013-05-05T01:19:53-05:00", + "thumbnails": [], + "output_media_files": [ + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 90, + "state": "finished", + "format": "mpeg4", + "audio_sample_rate": 44100, + "label": null, + "privacy": false, + "height": 720, + "error_message": null, + "url": "https://zencoder-temp-storage-us-east-1.s3.amazonaws.com/o/20130505/b22ad0c6353f86333126866d43cc898f/4f097ddbcee6c587cc640a6e99af2594.mp4?AWSAccessKeyId=AKIAI456JQ76GBU7FECA&Signature=QL0oZnKKivzEXlvSX3ealXDTI%2Bc%3D&Expires=1367821273", + "video_bitrate_in_kbps": 1440, + "md5_checksum": null, + "duration_in_ms": 5130, + "test": false, + "id": 93467532, + "finished_at": "2013-05-05T01:21:13-05:00", + "updated_at": "2013-05-05T01:21:14-05:00", + "created_at": "2013-05-05T01:19:53-05:00", + "total_bitrate_in_kbps": 1530, + "width": 1280, + "error_class": null, + "file_size_bytes": 973430 + } + ], + "pass_through": null + } + }, + { + "job": { + "submitted_at": "2013-05-05T01:21:21-05:00", + "state": "finished", + "privacy": false, + "input_media_file": { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 50, + "state": "finished", + "format": "mpeg4", + "audio_sample_rate": 44100, + "privacy": false, + "height": 720, + "error_message": null, + "url": "http://s3.amazonaws.com/zencodertesting/test.mov", + "video_bitrate_in_kbps": 1402, + "md5_checksum": null, + "duration_in_ms": 5067, + "test": false, + "id": 45472497, + "finished_at": "2013-05-05T01:21:28-05:00", + "updated_at": "2013-05-05T01:22:29-05:00", + "created_at": "2013-05-05T01:18:42-05:00", + "total_bitrate_in_kbps": 1452, + "width": 1280, + "error_class": null, + "file_size_bytes": 922620 + }, + "test": false, + "id": 45494508, + "finished_at": "2013-05-05T01:22:29-05:00", + "updated_at": "2013-05-05T01:22:29-05:00", + "created_at": "2013-05-05T01:18:42-05:00", + "thumbnails": [], + "output_media_files": [ + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 90, + "state": "finished", + "format": "mpeg4", + "audio_sample_rate": 44100, + "label": null, + "privacy": false, + "height": 720, + "error_message": null, + "url": "https://zencoder-temp-storage-us-east-1.s3.amazonaws.com/o/20130505/0ae6d5cbb960964a4714944cbc3e8bd9/7e082e00c717e2e6e32923500d3f43da.mp4?AWSAccessKeyId=AKIAI456JQ76GBU7FECA&Signature=PAAACDb22AiJOkxaq4h4pOIZWaQ%3D&Expires=1367821349", + "video_bitrate_in_kbps": 1440, + "md5_checksum": null, + "duration_in_ms": 5130, + "test": false, + "id": 93467424, + "finished_at": "2013-05-05T01:22:29-05:00", + "updated_at": "2013-05-05T01:22:29-05:00", + "created_at": "2013-05-05T01:18:42-05:00", + "total_bitrate_in_kbps": 1530, + "wi£dth": 1280, + "error_class": null, + "file_size_bytes": 973430 + } + ], + "pass_through": null + } + } +] \ No newline at end of file diff --git a/test/fixtures/job_list_limit.json b/test/fixtures/job_list_limit.json new file mode 100644 index 0000000..b51652b --- /dev/null +++ b/test/fixtures/job_list_limit.json @@ -0,0 +1,249 @@ +[ + { + "job": { + "submitted_at": "2013-05-05T01:30:15-05:00", + "state": "finished", + "privacy": false, + "stream": { + "state": "finished", + "height": 720, + "url": "rtmp://live40.us-va.zencoder.io:1935/live", + "duration": 13.3956291675568, + "name": "7177a51b45ccb2b594f890f99fef1fdc", + "test": false, + "id": 22915, + "finished_at": "2013-05-05T01:34:26-05:00", + "updated_at": "2013-05-05T01:35:14-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 1024, + "region": "us-virgina", + "width": 1280, + "protocol": "rtmp" + }, + "input_media_file": { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 131, + "state": "finished", + "format": "flash video", + "audio_sample_rate": 44100, + "privacy": false, + "height": 720, + "error_message": null, + "url": "rtmp://live40.us-va.zencoder.io:1935/live/republish/7177a51b45ccb2b594f890f99fef1fdc", + "video_bitrate_in_kbps": 1228, + "md5_checksum": null, + "duration_in_ms": 11090, + "test": false, + "id": 45472922, + "finished_at": "2013-05-05T01:34:32-05:00", + "updated_at": "2013-05-05T01:34:42-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 1359, + "width": 1280, + "error_class": null, + "file_size_bytes": 304313 + }, + "test": false, + "id": 45494934, + "finished_at": "2013-05-05T01:34:42-05:00", + "updated_at": "2013-05-05T01:34:42-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "thumbnails": [], + "output_media_files": [ + { + "video_codec": null, + "frame_rate": null, + "channels": null, + "audio_codec": null, + "audio_bitrate_in_kbps": null, + "state": "finished", + "format": null, + "audio_sample_rate": null, + "label": "hls_master", + "privacy": false, + "height": null, + "error_message": null, + "url": "http://hls.live.zencdn.net/hls/live/207996/77507/58d23f338d42277bbd74c6281627cea7/master.m3u8", + "video_bitrate_in_kbps": null, + "md5_checksum": null, + "duration_in_ms": null, + "test": false, + "id": 93468543, + "finished_at": "2013-05-05T01:34:35-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": null, + "width": null, + "error_class": null, + "file_size_bytes": 199 + }, + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 128, + "state": "finished", + "format": "mpeg-ts", + "audio_sample_rate": 44100, + "label": "hls_600", + "privacy": false, + "height": 360, + "error_message": null, + "url": "http://hls.live.zencdn.net/hls/live/207996/77507/58d23f338d42277bbd74c6281627cea7/600/index.m3u8", + "video_bitrate_in_kbps": 764, + "md5_checksum": null, + "duration_in_ms": 11100, + "test": false, + "id": 93468540, + "finished_at": "2013-05-05T01:34:39-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 892, + "width": 640, + "error_class": null, + "file_size_bytes": 1217265 + }, + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 128, + "state": "finished", + "format": "mpeg-ts", + "audio_sample_rate": 44100, + "label": "hls_300", + "privacy": false, + "height": 270, + "error_message": null, + "url": "http://hls.live.zencdn.net/hls/live/207996/77507/58d23f338d42277bbd74c6281627cea7/300/index.m3u8", + "video_bitrate_in_kbps": 400, + "md5_checksum": null, + "duration_in_ms": 11100, + "test": false, + "id": 93468539, + "finished_at": "2013-05-05T01:34:40-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 528, + "width": 480, + "error_class": null, + "file_size_bytes": 708537 + }, + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 128, + "state": "finished", + "format": "mpeg-ts", + "audio_sample_rate": 44100, + "label": "hls_1200", + "privacy": false, + "height": 720, + "error_message": null, + "url": "http://hls.live.zencdn.net/hls/live/207996/77507/58d23f338d42277bbd74c6281627cea7/1200/index.m3u8", + "video_bitrate_in_kbps": 1484, + "md5_checksum": null, + "duration_in_ms": 11100, + "test": false, + "id": 93468542, + "finished_at": "2013-05-05T01:34:40-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 1612, + "width": 1280, + "error_class": null, + "file_size_bytes": 2223629 + }, + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 124, + "state": "finished", + "format": "flash video", + "audio_sample_rate": 44100, + "label": "rtmp_300", + "privacy": false, + "height": 270, + "error_message": null, + "url": "rtmp://rtmp.live.zencdn.net/live/15ec8791b4d951a6053de2799170ec93_77507_300@107413", + "video_bitrate_in_kbps": 343, + "md5_checksum": null, + "duration_in_ms": 11160, + "test": false, + "id": 93468534, + "finished_at": "2013-05-05T01:34:41-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 467, + "width": 480, + "error_class": null, + "file_size_bytes": 667832 + }, + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 124, + "state": "finished", + "format": "flash video", + "audio_sample_rate": 44100, + "label": "rtmp_600", + "privacy": false, + "height": 360, + "error_message": null, + "url": "rtmp://rtmp.live.zencdn.net/live/cd5766181ed19b81195db56092b4e500_77507_600@107415", + "video_bitrate_in_kbps": 690, + "md5_checksum": null, + "duration_in_ms": 11160, + "test": false, + "id": 93468535, + "finished_at": "2013-05-05T01:34:41-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 814, + "width": 640, + "error_class": null, + "file_size_bytes": 1152631 + }, + { + "video_codec": "h264", + "frame_rate": 30, + "channels": "2", + "audio_codec": "aac", + "audio_bitrate_in_kbps": 124, + "state": "finished", + "format": "flash video", + "audio_sample_rate": 44100, + "label": "rtmp_1200", + "privacy": false, + "height": 720, + "error_message": null, + "url": "rtmp://rtmp.live.zencdn.net/live/a83a306ea10a266e027c3186bff701b3_77507_1200@107417", + "video_bitrate_in_kbps": 1352, + "md5_checksum": null, + "duration_in_ms": 11160, + "test": false, + "id": 93468537, + "finished_at": "2013-05-05T01:34:42-05:00", + "updated_at": "2013-05-05T01:34:43-05:00", + "created_at": "2013-05-05T01:30:15-05:00", + "total_bitrate_in_kbps": 1476, + "width": 1280, + "error_class": null, + "file_size_bytes": 2076855 + } + ], + "pass_through": null + } + } +] \ No newline at end of file diff --git a/test/fixtures/job_progress.json b/test/fixtures/job_progress.json new file mode 100644 index 0000000..0dfdc90 --- /dev/null +++ b/test/fixtures/job_progress.json @@ -0,0 +1,17 @@ +{ + "state": "processing", + "input": { + "state": "finished", + "id": 45474984 + }, + "progress": 40.5, + "outputs": [ + { + "state": "processing", + "id": 93474209, + "current_event_progress": 0, + "progress": 15, + "current_event": "Transcoding" + } + ] +} diff --git a/test/fixtures/output_details.json b/test/fixtures/output_details.json new file mode 100644 index 0000000..239afa3 --- /dev/null +++ b/test/fixtures/output_details.json @@ -0,0 +1,20 @@ +{ + "audio_bitrate_in_kbps": 74, + "audio_codec": "aac", + "audio_sample_rate": 48000, + "channels": "2", + "duration_in_ms": 24892, + "file_size_in_bytes": 1215110, + "format": "mpeg4", + "frame_rate": 29.97, + "height": 352, + "id": 13339, + "label": null, + "state": "finished", + "total_bitrate_in_kbps": 387, + "url": "https://example.com/file.mp4", + "video_bitrate_in_kbps": 313, + "video_codec": "h264", + "width": 624, + "md5_checksum": "7f106918e02a69466afa0ee014174143" +} \ No newline at end of file diff --git a/test/fixtures/output_progress.json b/test/fixtures/output_progress.json new file mode 100644 index 0000000..54da2f6 --- /dev/null +++ b/test/fixtures/output_progress.json @@ -0,0 +1,6 @@ +{ + "state": "processing", + "current_event": "Transcoding", + "current_event_progress": 45.32525, + "progress": 32.34567345 +} \ No newline at end of file diff --git a/test/test_accounts.py b/test/test_accounts.py new file mode 100644 index 0000000..36debf8 --- /dev/null +++ b/test/test_accounts.py @@ -0,0 +1,55 @@ +import unittest +from mock import patch + +from test_util import TEST_API_KEY, load_response +from zencoder import Zencoder + +class TestAccounts(unittest.TestCase): + def setUp(self): + self.zen = Zencoder(api_key=TEST_API_KEY) + + @patch("requests.Session.post") + def test_account_create(self, post): + post.return_value = load_response(201, 'fixtures/account_create.json') + + response = self.zen.account.create('test@example.com', tos=1) + + self.assertEquals(response.code, 201) + self.assertEquals(response.body['password'], 'foo') + self.assertEquals(response.body['api_key'], 'abcd1234') + + @patch("requests.Session.get") + def test_account_details(self, get): + get.return_value = load_response(200, 'fixtures/account_details.json') + resp = self.zen.account.details() + + self.assertEquals(resp.code, 200) + self.assertEquals(resp.body['account_state'], 'active') + self.assertEquals(resp.body['minutes_used'], 12549) + + @patch("requests.Session.put") + def test_account_integration(self, put): + put.return_value = load_response(204) + + resp = self.zen.account.integration() + + self.assertEquals(resp.code, 204) + self.assertEquals(resp.body, None) + + @patch("requests.Session.put") + def test_account_live_unauthorized(self, put): + put.return_value = load_response(402) + + resp = self.zen.account.live() + self.assertEquals(resp.code, 402) + + @patch("requests.Session.put") + def test_account_live_authorized(self, put): + put.return_value = load_response(204) + + resp = self.zen.account.live() + self.assertEquals(resp.code, 204) + +if __name__ == "__main__": + unittest.main() + diff --git a/test/test_jobs.py b/test/test_jobs.py new file mode 100644 index 0000000..f32d57c --- /dev/null +++ b/test/test_jobs.py @@ -0,0 +1,90 @@ +import unittest +from mock import patch + +from test_util import TEST_API_KEY, load_response +from zencoder import Zencoder + +class TestJobs(unittest.TestCase): + + def setUp(self): + self.zen = Zencoder(api_key=TEST_API_KEY) + + @patch("requests.Session.post") + def test_job_create(self, post): + post.return_value = load_response(201, 'fixtures/job_create.json') + + resp = self.zen.job.create('s3://zencodertesting/test.mov') + + self.assertEquals(resp.code, 201) + self.assertTrue(resp.body['id'] > 0) + self.assertEquals(len(resp.body['outputs']), 1) + + @patch("requests.Session.post") + def test_job_create_list(self, post): + post.return_value = load_response(201, 'fixtures/job_create_live.json') + + resp = self.zen.job.create(live_stream=True) + + self.assertEquals(resp.code, 201) + self.assertTrue(resp.body['id'] > 0) + self.assertEquals(len(resp.body['outputs']), 1) + + @patch("requests.Session.get") + def test_job_details(self, get): + get.return_value = load_response(200, 'fixtures/job_details.json') + + resp = self.zen.job.details(1234) + self.assertEquals(resp.code, 200) + self.assertTrue(resp.body['job']['id'] > 0) + self.assertEquals(len(resp.body['job']['output_media_files']), 1) + + @patch("requests.Session.get") + def test_job_progress(self, get): + get.return_value = load_response(200, 'fixtures/job_progress.json') + + resp = self.zen.job.progress(12345) + self.assertEquals(resp.code, 200) + self.assertEquals(resp.body['state'], 'processing') + + @patch("requests.Session.put") + def test_job_cancel(self, put): + put.return_value = load_response(204) + + resp = self.zen.job.cancel(5555) + self.assertEquals(resp.code, 204) + self.assertEquals(resp.body, None) + + @patch("requests.Session.put") + def test_job_resubmit(self, put): + put.return_value = load_response(204) + + resp = self.zen.job.resubmit(5555) + self.assertEquals(resp.code, 204) + self.assertEquals(resp.body, None) + + @patch("requests.Session.get") + def test_job_list(self, get): + get.return_value = load_response(200, 'fixtures/job_list.json') + + resp = self.zen.job.list() + self.assertEquals(resp.code, 200) + self.assertEquals(len(resp.body), 3) + + @patch("requests.Session.get") + def test_job_list_limit(self, get): + get.return_value = load_response(200, 'fixtures/job_list_limit.json') + + resp = self.zen.job.list(per_page=1) + self.assertEquals(resp.code, 200) + self.assertEquals(len(resp.body), 1) + + @patch("requests.Session.put") + def test_job_finish(self, put): + put.return_value = load_response(204) + + resp = self.zen.job.finish(99999) + self.assertEquals(resp.code, 204) + +if __name__ == "__main__": + unittest.main() + diff --git a/test/test_outputs.py b/test/test_outputs.py new file mode 100644 index 0000000..e0628e3 --- /dev/null +++ b/test/test_outputs.py @@ -0,0 +1,31 @@ +import unittest +from zencoder import Zencoder + +from mock import patch + +from test_util import TEST_API_KEY, load_response +from zencoder import Zencoder + +class TestOutputs(unittest.TestCase): + def setUp(self): + self.zen = Zencoder(api_key=TEST_API_KEY) + + @patch("requests.Session.get") + def test_output_details(self, get): + get.return_value = load_response(200, 'fixtures/output_details.json') + + resp = self.zen.output.details(22222) + self.assertEquals(resp.code, 200) + self.assertTrue(resp.body['id'] > 0) + + @patch("requests.Session.get") + def test_output_progress(self, get): + get.return_value = load_response(200, 'fixtures/output_progress.json') + + resp = self.zen.output.progress(123456) + self.assertEquals(resp.code, 200) + self.assertEquals(resp.body['state'], 'processing') + +if __name__ == "__main__": + unittest.main() + diff --git a/test/test_util.py b/test/test_util.py new file mode 100644 index 0000000..f985ca9 --- /dev/null +++ b/test/test_util.py @@ -0,0 +1,19 @@ +from collections import namedtuple +import json +import os + +TEST_API_KEY = 'abcd123' + +MockResponse = namedtuple("Response", "status_code, json, content") + +CUR_DIR = os.path.split(__file__)[0] + +def load_response(code, fixture=None): + if fixture: + with open(os.path.join(CUR_DIR, fixture), 'r') as f: + content = f.read() + else: + content = None + + return MockResponse(code, lambda: json.loads(content), content) + diff --git a/test/test_zencoder.py b/test/test_zencoder.py index 565d306..02c461f 100644 --- a/test/test_zencoder.py +++ b/test/test_zencoder.py @@ -34,12 +34,6 @@ def test_set_api_edge_version(self): zc = Zencoder(api_version='edge') self.assertEquals(zc.base_url, 'https://app.zencoder.com/api/') - def test_zero_content_length(self): - os.environ['ZENCODER_API_KEY'] = 'abcd123' - zc = Zencoder() - content = None - self.assertEquals(zc.job.content_length(content), "0") - if __name__ == "__main__": unittest.main() diff --git a/zencoder/__init__.py b/zencoder/__init__.py index 28a00eb..b6c48ac 100644 --- a/zencoder/__init__.py +++ b/zencoder/__init__.py @@ -1,7 +1,8 @@ -from core import Zencoder -from core import ZencoderResponseError +from .core import Zencoder +from .core import ZencoderResponseError +from .core import Account +from .core import __version__ -from core import Account - -from core import __version__ +__title__ = 'zencoder' +__author__ = 'Alex Schworer' diff --git a/zencoder/core.py b/zencoder/core.py index 25d8cb7..e34f158 100644 --- a/zencoder/core.py +++ b/zencoder/core.py @@ -1,6 +1,5 @@ import os -import httplib2 -from urllib import urlencode +import requests from datetime import datetime # Note: I've seen this pattern for dealing with json in different versions of @@ -30,90 +29,52 @@ def __init__(self, http_response, content): self.content = content class HTTPBackend(object): - """ - Abstracts out an HTTP backend, but defaults to httplib2. Required arguments - are `base_url` and `api_key`. - - .. note:: - While `as_xml` is provided as a keyword argument, XML or input or output - is not supported. - """ - def __init__(self, base_url, api_key, as_xml=False, resource_name=None, timeout=None, test=False, version=None): + """ Abstracts out an HTTP backend. Required argument are `base_url` and + `api_key`. """ + def __init__(self, + base_url, + api_key, + resource_name=None, + timeout=None, + test=False, + version=None): + self.base_url = base_url + if resource_name: self.base_url = self.base_url + resource_name - self.http = httplib2.Http(timeout=timeout) - self.as_xml = as_xml + self.http = requests.Session() + self.api_key = api_key self.test = test self.version = version - def content_length(self, body): - """ - Returns the content length as an int for the given `body` data. Used by - PUT and POST requests to set the Content-Length header. - """ - return str(len(body)) if body else "0" + # sets request headers for the entire session + self.http.headers.update(self.headers) @property def headers(self): - """ Returns default headers, by setting the Content-Type and Accepts - headers. - """ - content_type = 'xml' if self.as_xml else 'json' + """ Returns default headers, by setting the Content-Type, Accepts, + User-Agent and API Key headers.""" headers = { - 'Content-Type': 'application/{0}'.format(content_type), - 'Accept': 'application/{0}'.format(content_type), + 'Content-Type': 'application/json', + 'Accept': 'application/json', 'Zencoder-Api-Key': self.api_key, 'User-Agent': 'zencoder-py v{0}'.format(__version__) } return headers - def encode(self, data): - """ - Encodes data as JSON (by calling `json.dumps`), so that it can be - passed onto the Zencoder API. - - .. note:: - Encoding as XML is not supported. - """ - if not self.as_xml: - return json.dumps(data) - else: - raise NotImplementedError('Encoding as XML is not supported.') - - def decode(self, raw_body): - """ - Returns the JSON-encoded `raw_body` and decodes it to a `dict` (using - `json.loads`). - - .. note:: - Decoding as XML is not supported. - """ - if not self.as_xml: - # only parse json when it exists, else just return None - if not raw_body or raw_body == ' ': - return None - else: - return json.loads(raw_body) - else: - raise NotImplementedError('Decoding as XML is not supported.') - def delete(self, url, params=None): """ Executes an HTTP DELETE request for the given URL - params should be a urllib.urlencoded string + params should be a dictionary """ - if params: - url = '?'.join([url, params]) - - response, content = self.http.request(url, method="DELETE", - headers=self.headers) - return self.process(response, content) + response = self.http.delete(url, params=params) + return self.process(response) def get(self, url, data=None): """ @@ -121,70 +82,58 @@ def get(self, url, data=None): data should be a dictionary of url parameters """ - if data: - params = urlencode(data) - url = '?'.join([url, params]) - - response, content = self.http.request(url, method="GET", - headers=self.headers) - return self.process(response, content) + response = self.http.get(url, headers=self.headers, params=data) + return self.process(response) def post(self, url, body=None): """ Executes an HTTP POST request for the given URL """ - headers = self.headers - headers['Content-Length'] = self.content_length(body) - response, content = self.http.request(url, method="POST", - body=body, - headers=self.headers) + response = self.http.post(url, data=body, headers=self.headers) - return self.process(response, content) + return self.process(response) def put(self, url, data=None, body=None): """ Executes an HTTP PUT request for the given URL """ - headers = self.headers - headers['Content-Length'] = self.content_length(body) + response = self.http.put(url, params=data, data=body, headers=self.headers) - if data: - params = urlencode(data) - url = '?'.join([url, params]) + return self.process(response) - response, content = self.http.request(url, method="PUT", - body=body, - headers=headers) - - return self.process(response, content) - - def process(self, http_response, content): - """ - Returns HTTP backend agnostic Response data - """ + def process(self, response): + """ Returns HTTP backend agnostic Response data. """ try: - code = http_response.status - body = self.decode(content) - response = Response(code, body, content, http_response) - - return response + code = response.status_code + + # 204 - No Content + if code == 204: + body = None + # add an error message to 402 errors + elif code == 402: + body = { + "message": "Payment Required", + "status": "error" + } + else: + body = response.json() + return Response(code, body, response.content, response) except ValueError: - raise ZencoderResponseError(http_response, content) + raise ZencoderResponseError(response, response.content) class Zencoder(object): """ This is the entry point to the Zencoder API """ - def __init__(self, api_key=None, api_version=None, as_xml=False, timeout=None, test=False): + def __init__(self, api_key=None, api_version=None, timeout=None, test=False): """ - Initializes Zencoder. You must have a valid API_KEY. + Initializes Zencoder. You must have a valid `api_key`. You can pass in the api_key as an argument, or set `ZENCODER_API_KEY` as an environment variable, and it will use - that, if api_key is unspecified. + that, if `api_key` is unspecified. Set api_version='edge' to get the Zencoder development API. (defaults to 'v2') - Set as_xml=True to get back xml data instead of the default json. """ if not api_version: api_version = 'v2' @@ -202,9 +151,8 @@ def __init__(self, api_key=None, api_version=None, as_xml=False, timeout=None, t self.api_key = api_key self.test = test - self.as_xml = as_xml - args = (self.base_url, self.api_key, self.as_xml) + args = (self.base_url, self.api_key) kwargs = dict(timeout=timeout, test=self.test, version=api_version) self.job = Job(*args, **kwargs) self.account = Account(*args, **kwargs) @@ -214,10 +162,7 @@ def __init__(self, api_key=None, api_version=None, as_xml=False, timeout=None, t self.report = Report(*args, **kwargs) class Response(object): - """ - The Response object stores the details of an API request in an XML/JSON - agnostic way. - """ + """ The Response object stores the details of an API request. """ def __init__(self, code, body, raw_body, raw_response): self.code = code self.body = body @@ -242,7 +187,7 @@ def create(self, email, tos=1, options=None): if options: data.update(options) - return self.post(self.base_url, body=self.encode(data)) + return self.post(self.base_url, body=json.dumps(data)) def details(self): """ @@ -262,7 +207,6 @@ def live(self): """ Puts your account into live mode. """ - return self.put(self.base_url + '/live') class Output(HTTPBackend): @@ -287,13 +231,9 @@ def details(self, output_id): return self.get(self.base_url + '/%s' % str(output_id)) class Job(HTTPBackend): - """ - Contains all API methods relating to transcoding Jobs. - """ + """ Contains all API methods relating to transcoding Jobs. """ def __init__(self, *args, **kwargs): - """ - Initializes a job object - """ + """ Initializes a job object. """ kwargs['resource_name'] = 'jobs' super(Job, self).__init__(*args, **kwargs) @@ -302,7 +242,7 @@ def create(self, input=None, live_stream=False, outputs=None, options=None): Creates a transcoding job. @param input: the input url as string - @param live_stream: starts an RTMP Live Stream + @param live_stream: starts a Live Stream job (input must be None) @param outputs: a list of output dictionaries @param options: a dictionary of job options """ @@ -313,7 +253,10 @@ def create(self, input=None, live_stream=False, outputs=None, options=None): if options: data.update(options) - return self.post(self.base_url, body=self.encode(data)) + if live_stream: + data['live_stream'] = live_stream + + return self.post(self.base_url, body=json.dumps(data)) def list(self, page=1, per_page=50): """