├── .gitignore ├── CHANGES.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── TODO.md ├── gtmetrix ├── __init__.py ├── exceptions.py ├── interface.py ├── settings.py └── utils.py ├── pytest.ini ├── requirements.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── test_gtmetrix_interface.py ├── test_gtmetrix_settings.py └── utils.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | dist/ 3 | build/ 4 | *.egg-info/ 5 | .cache/ 6 | .eggs/ 7 | .idea/ 8 | __pycache__ 9 | .tox/ 10 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | March 25, 2017 -- ssteinerX 2 | --------------------------- 3 | Refactored and simplified tests while making them more thorough. 4 | 5 | Updated tests to run under [tox](https://tox.readthedocs.io/en/latest/) and 6 | separated the inherited runtime environment from the tests. 7 | 8 | Tests currently pass on all the environments in which the project is 9 | supposed to run: 2.7, 3.5, and 3.6. 10 | 11 | Updated docs to show how to run tests and how to work in development mode for 12 | anyone who wants to contribute to the project. 13 | 14 | February 4, 2017 -- ssteinerX 15 | ----------------------------- 16 | Add setup.cfg and rearrange setup.py to use current pytest conventions and pytest-runner plugin 17 | 18 | Remove Bitdeli badge from README.md, [Bitdeli acquired and shut down](https://www.linkedin.com/in/villetuulos) 19 | 20 | > Bitdeli was powered ... [Ville Tuulos] was the CEO. 21 | > Bitdeli was acquired by AdRoll in June 2013. 22 | 23 | February 3, 2017 -- ssteinerX 24 | ----------------------------- 25 | Create add-unit-tests branch to add some unit tests in anticipation of 26 | full Python 3.x compatibility. 27 | 28 | Add this CHANGES.md file to keep track of changes for detail beyond commit messages. 29 | 30 | Add specific Python versions to setup.py. 31 | 32 | Add test class and associated test_* support items to setup.py. 33 | 34 | Add version constraints to install_requires to ensure Python 3 compatible 35 | versions of dependencies. 36 | 37 | Update .gitignore to skip .cache/, .eggs/, and .idea (PyCharm) files. 38 | 39 | Add tests module at root level to contain test suite. 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Alex Isayko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt *.py 2 | exclude *.pyc 3 | recursive-include gtmetrix * 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-gtmetrix 2 | ======================== 3 | 4 | **python-gtmetrix** is a Python client library for GTmetrix REST API. 5 | 6 | Quickstart: 7 | ======================== 8 | 9 | Install package: 10 | 11 | $ pip install python-gtmetrix 12 | 13 | 14 | Make tests: 15 | ----------- 16 | 17 | Create json with urls: 18 | 19 | { 20 | "google": "http://google.com", 21 | } 22 | 23 | Start test using json with urls: 24 | 25 | from gtmetrix import * 26 | import json 27 | 28 | with open('sites.json') as data_file: 29 | list_sites = json.load(data_file) 30 | 31 | for key, value in list_sites.items(): 32 | print ("Site: %s - Url: %s" % (key, value)) 33 | gt = GTmetrixInterface('your@email.com', 'api-key') 34 | my_test = gt.start_test(value) 35 | 36 | 37 | Fetch results: 38 | 39 | my_test.fetch_results(key) 40 | 41 | or 42 | 43 | results = gt.poll_state_request(key, my_test.test_id) 44 | 45 | When test is completed you able to access next data in the files: 46 | 47 | results-date = pagespeed_score, yslow_score, page_bytes, page_load_time, page_elements 48 | resources-date= urls to screenshot, har, pagespeed_url, pagespeed_files, yslow_url, report_pdf, report_pdf_full 49 | 50 | 51 | 52 | List of available params and response attributes you can find at http://gtmetrix.com/api/ 53 | 54 | 55 | Exceptions: 56 | ----------- 57 | 58 | Invalid test request 59 | 60 | GTmetrixInvalidTestRequest 61 | 62 | The requested test does not exist 63 | 64 | GTmetrixTestNotFound 65 | 66 | The maximum number of API calls reached 67 | 68 | GTmetrixMaximumNumberOfApis 69 | 70 | Too many concurrent requests from your IP 71 | 72 | GTmetrixManyConcurrentRequests 73 | 74 | Example: 75 | 76 | from gtmetrix import * 77 | 78 | gt = GTmetrixInterface('your@email.com', 'api-key') 79 | 80 | try: 81 | my_test = gt.start_test('http://google.com') 82 | except GTmetrixInvalidTestRequest: 83 | print 'Something goes wrong' 84 | 85 | try: 86 | results = gt.poll_state_request(my_test.test_id) 87 | except GTmetrixTestNotFound: 88 | raise Http404 89 | 90 | Running Tests & Development: 91 | ---------------------------- 92 | 93 | In order to run the tests you can either checkout from the main repo at 94 | [GitHub](https://github.com/aisayko/python-gtmetrix.git), or download the 95 | zip file or what have you. 96 | 97 | Create a new virtual environment, however you do that, and run: 98 | 99 | pip install -r requirements.txt 100 | 101 | Then, make it so you're running off your checkout: 102 | 103 | python setup.py develop 104 | 105 | Run the tests to make sure you're at a good starting point. 106 | 107 | The tests are currently run against Python 2.7.x, Python 3.5.x, and Python 3 108 | .6.x. Do this with: 109 | 110 | tox 111 | 112 | You should see: 113 | 114 | ____________________ summary ____________________ 115 | py27: commands succeeded 116 | py35: commands succeeded 117 | py36: commands succeeded 118 | congratulations :) 119 | 120 | At this point, you will be testing your own checkout and can proceed to fix 121 | bugs, improve documentation, etc. For a list of what's needed, see the 122 | [issue tracker](https://github.com/aisayko/python-gtmetrix/issues). 123 | 124 | 125 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Python GTmetrix API Library 2 | ### TODO list 3 | 4 | 5 | ##### February 4, 2017 -- ssteinerX 6 | 7 | 1> Exceptions that have default messages shouldn't rep repeat the doc string as the message. Could use inspect.getdoc or write little decorator. I know I've done this before, have to find it. 8 | 9 | 2> Generate docs with Sphinx (NOTE: I don't use .rst, Markdown support hints here: http://stackoverflow.com/questions/2471804/using-sphinx-with-markdown-instead-of-rst) 10 | 11 | 3> Publish docs to ReadTheDocs 12 | 13 | 4> Integrate Tox&Travis @Github 14 | -------------------------------------------------------------------------------- /gtmetrix/__init__.py: -------------------------------------------------------------------------------- 1 | from gtmetrix.interface import * 2 | from gtmetrix.utils import * 3 | -------------------------------------------------------------------------------- /gtmetrix/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python GTmetrix API Library 3 | 4 | Exceptions 5 | 6 | TODO: ss -- rearrange __all__ and code to match and alphabetical order 7 | """ 8 | __all__ = ['GTmetrixInvalidTestRequest', 9 | 'GTmetrixAPIUrlIsNone', 10 | 'GTmetrixBadAPIUrl', 11 | 'GTmetrixAPIKeyIsNone', 12 | 'GTmetrixEmailIsNone', 13 | 'GTmetrixInvalidEmail', 14 | 'GTmetrixEmailIsNotStringtype', 15 | 'GTmetrixMissingEmailOrAPIKey', 16 | 'GTmetrixManyConcurrentRequests', 17 | 'GTmetrixMaximumNumberOfApis', 18 | 'GTmetrixTestNotFound', 19 | ] 20 | 21 | 22 | class GTmetrixBadAPIUrl(Exception): 23 | """URL is not the same as what was in settings.""" 24 | pass 25 | 26 | 27 | class GTmetrixAPIUrlIsNone(Exception): 28 | """The URL is `None`.""" 29 | pass 30 | 31 | 32 | class GTmetrixAPIKeyIsNone(Exception): 33 | """API key is not in settings or passed to initializer.""" 34 | 35 | def __init__(self, message=None): 36 | self.message = (message or 37 | "API key must be passed or exist in the settings.") 38 | 39 | 40 | class GTmetrixEmailIsNone(Exception): 41 | """Email is not in settings or passed to initializer.""" 42 | 43 | def __init__(self, message=None): 44 | self.message = (message or 45 | "Email must be passed or exist in the settings.") 46 | 47 | 48 | class GTmetrixEmailIsNotStringtype(Exception): 49 | """Email is not a string type.""" 50 | 51 | def __init__(self, message=None): 52 | self.message = (message or 53 | "Email is not a stringtype.") 54 | 55 | 56 | class GTmetrixInvalidEmail(Exception): 57 | """Email is invalid.""" 58 | 59 | def __init__(self, message=None): 60 | self.message = (message or 61 | "Email is invalid.") 62 | 63 | 64 | class GTmetrixMissingEmailOrAPIKey(Exception): 65 | """Email or API key is not in settings or passed to initializer. 66 | NOTE: this exception isn't used in actual interface due to specific 67 | exceptions for invalid email/key/url taking precedence. This is 68 | used only to validate when invalid test data is produced while 69 | testing. 70 | """ 71 | 72 | def __init__(self, message=None): 73 | self.message = (message or 74 | "Email and API key must be passed or exist in the settings.") 75 | 76 | 77 | class GTmetrixInvalidTestRequest(Exception): 78 | """Invalid test request.""" 79 | pass 80 | 81 | 82 | class GTmetrixManyConcurrentRequests(Exception): 83 | """Too many concurrent requests from your IP.""" 84 | pass 85 | 86 | 87 | class GTmetrixMaximumNumberOfApis(Exception): 88 | """The maximum number of API calls reached.""" 89 | pass 90 | 91 | 92 | class GTmetrixTestNotFound(Exception): 93 | """The requested test does not exist.""" 94 | pass 95 | -------------------------------------------------------------------------------- /gtmetrix/interface.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os.path 3 | import time 4 | import datetime 5 | 6 | from gtmetrix import settings 7 | from gtmetrix.exceptions import * 8 | from gtmetrix.utils import (validate_email, 9 | validate_api_key) 10 | 11 | __all__ = ['GTmetrixInterface',] 12 | 13 | 14 | class _TestObject(object): 15 | """GTmetrix Test representation.""" 16 | STATE_QUEUED = 'queued' 17 | STATE_STARTED = 'started' 18 | STATE_COMPLETED = 'completed' 19 | STATE_ERROR = 'error' 20 | 21 | def __init__(self, auth, test_id, poll_state_url=None, credits_left=None): 22 | self.poll_state_url = (poll_state_url or 23 | os.path.join(settings.GTMETRIX_REST_API_URL, test_id)) 24 | self.test_id = test_id 25 | self.credits_left = credits_left 26 | self.state = self.STATE_QUEUED 27 | self.credits_left = 0 28 | self.auth = auth 29 | self.results = {} 30 | self.resources = {} 31 | self.pagespeed_score = {} 32 | self.yslow_score = {} 33 | self.html_bytes = {} 34 | self.html_load_time = {} 35 | self.page_bytes = {} 36 | self.page_load_time = {} 37 | self.page_elements ={} 38 | self.redirect_duration = {} 39 | self.connect_duration = {} 40 | self.backend_duration = {} 41 | self.first_paint_time = {} 42 | self.first_contentful_paint_time = {} 43 | self.dom_interactive_time = {} 44 | self.dom_content_loaded_time = {} 45 | self.dom_content_loaded_duration = {} 46 | self.onload_time = {} 47 | self.onload_duration = {} 48 | self.fully_loaded_time = {} 49 | self.rum_speed_index = {} 50 | self.screenshot ={} 51 | self.har = {} 52 | self.pagespeed_url ={} 53 | self.pagespeed_files = {} 54 | self.yslow_url = {} 55 | self.report_pdf = {} 56 | self.report_pdf_full = {} 57 | 58 | def _request(self, url): 59 | response = requests.get(url, auth=self.auth) 60 | response_data = response.json() 61 | 62 | if response.status_code == 404: 63 | raise GTmetrixTestNotFound(response_data['error']) 64 | 65 | if response.status_code == 400: 66 | raise GTmetrixInvalidTestRequest(response_data['error']) 67 | 68 | if response.status_code == 402: 69 | raise GTmetrixMaximumNumberOfApis(response_data['error']) 70 | 71 | if response.status_code == 429: 72 | raise GTmetrixManyConcurrentRequests(response_data['error']) 73 | 74 | return response_data 75 | 76 | def fetch_results(self, key): 77 | """Get the test state and results/resources (when test complete).""" 78 | response_data = self._request(self.poll_state_url) 79 | 80 | self.state = response_data['state'] 81 | 82 | number_executions = 0 83 | while not self.state == self.STATE_COMPLETED and (number_executions < 30): 84 | number_executions += 1 85 | time.sleep(30) 86 | response_data = self._request(self.poll_state_url) 87 | self.state = response_data['state'] 88 | 89 | self._extract_results(response_data, key) 90 | 91 | return response_data 92 | 93 | def _get_result( self, key, dflt='' ): 94 | return self.results[ key ] if key in self.results else dflt 95 | 96 | def _get_resource( self, key, dflt='' ): 97 | return self.resources[ key ] if key in self.resources else dflt 98 | 99 | def _extract_results(self, response_data, key): 100 | today = datetime.datetime.now() 101 | day = today.day 102 | month = today.month 103 | year = today.year 104 | if 'results' in response_data: 105 | self.results = response_data['results'] 106 | self.pagespeed_score = self._get_result( 'pagespeed_score' ) 107 | self.yslow_score = self._get_result( 'yslow_score' ) 108 | self.html_bytes = self._get_result( 'html_bytes' ) 109 | self.html_load_time = self._get_result( 'html_load_time' ) 110 | self.page_bytes = self._get_result( 'page_bytes' ) 111 | self.page_load_time = self._get_result( 'page_load_time' ) 112 | self.page_elements = self._get_result( 'page_elements' ) 113 | self.redirect_duration = self._get_result( 'redirect_duration' ) 114 | self.connect_duration = self._get_result( 'connect_duration' ) 115 | self.backend_duration = self._get_result( 'backend_duration' ) 116 | self.first_paint_time = self._get_result( 'first_paint_time' ) 117 | self.first_contentful_paint_time = self._get_result( 'first_contentful_paint_time' ) 118 | self.dom_interactive_time = self._get_result( 'dom_interactive_time' ) 119 | self.dom_content_loaded_time = self._get_result( 'dom_content_loaded_time' ) 120 | self.dom_content_loaded_duration = self._get_result( 'dom_content_loaded_duration' ) 121 | self.onload_time = self._get_result( 'onload_time' ) 122 | self.onload_duration = self._get_result( 'onload_duration' ) 123 | self.fully_loaded_time = self._get_result( 'fully_loaded_time' ) 124 | self.rum_speed_index = self._get_result( 'rum_speed_index' ) 125 | #name_of_file_results = "results-%d-%d-%d" % (day,month, year) 126 | #file = open(name_of_file_results, "a") 127 | #file.write("site:%s pagespeed_score:%s yslow_score:%s page_load_time:%s page_bytes:%s page_elements:%s \n" % (key, self.pagespeed_score, self.yslow_score, self.page_load_time, self.page_bytes, self.page_elements)) 128 | #file.close() 129 | 130 | if 'resources' in response_data: 131 | self.resources = response_data['resources'] 132 | self.screenshot = self._get_resource( 'screenshot' ) 133 | self.har = self._get_resource( 'har' ) 134 | self.pagespeed_url = self._get_resource( 'pagespeed' ) 135 | self.pagespeed_files = self._get_resource( 'pagespeed_files' ) 136 | self.yslow_url = self._get_resource( 'yslow' ) 137 | self.report_pdf = self._get_resource( 'report_pdf' ) 138 | self.report_pdf_full = self._get_resource( 'report_pdf_full' ) 139 | #name_of_file_resources = "resources-%d-%d-%d" % (day,month, year) 140 | #file = open(name_of_file_resources, "a") 141 | #file.write("site:%s screenshot:%s har:%s pagespeed_url:%s pagespeed_files:%s yslow_url:%s report_pdf:%s report_pdf_full:%s \n" % (key, self.screenshot, self.har, self.pagespeed_url, self.pagespeed_files, self.yslow_url, self.report_pdf, self.report_pdf_full)) 142 | #file.close() 143 | 144 | 145 | 146 | class GTmetrixInterface(object): 147 | """Provides an interface to access GTmetrix REST API.""" 148 | def __init__(self, user_email=None, api_key=None): 149 | # Validate and set instance variables 150 | self.set_auth_email_and_key(user_email, api_key) 151 | self.auth = (self.user_email, self.api_key) 152 | 153 | def set_auth_email_and_key(self, user_email=None, api_key=None): 154 | # Get from params or default to values from settings 155 | self.user_email = user_email or settings.GTMETRIX_REST_API_EMAIL 156 | self.api_key = api_key or settings.GTMETRIX_REST_API_KEY 157 | 158 | # Make sure they're valid 159 | self.validate_user_email() 160 | self.validate_api_key() 161 | 162 | def validate_user_email(self): 163 | """Hook for user email validation.""" 164 | return validate_email(self.user_email) 165 | 166 | def validate_api_key(self): 167 | """Hook for api key validation.""" 168 | return validate_api_key(self.api_key) 169 | 170 | def start_test(self, url, **data): 171 | """ Start a Test """ 172 | data.update({'url': url}) 173 | response = requests.post(settings.GTMETRIX_REST_API_URL, data=data, auth=self.auth) 174 | response_data = response.json() 175 | 176 | if response.status_code != 200: 177 | raise GTmetrixInvalidTestRequest(response_data['error']) 178 | 179 | return _TestObject(self.auth, **response_data) 180 | 181 | def poll_state_request(self, key, test_id): 182 | test = _TestObject(self.auth, test_id) 183 | test.fetch_results(key) 184 | return test 185 | -------------------------------------------------------------------------------- /gtmetrix/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | The following settings should be defined in your global settings 3 | """ 4 | import os 5 | 6 | GTMETRIX_REST_API_URL = 'https://gtmetrix.com/api/0.1/test' 7 | 8 | # API calls will default to using these if an explicit value is not 9 | # passed to GTmetrixInterface() 10 | GTMETRIX_REST_API_EMAIL = os.getenv('GTMETRIX_REST_API_EMAIL', None) 11 | GTMETRIX_REST_API_KEY = os.getenv('GTMETRIX_REST_API_KEY', None) 12 | 13 | # TODO: when validation is tighter, these might not pass. The tests 14 | # will fail then, just like they're supposed to. 15 | good_email = "example@example.com" 16 | good_apikey = "abcdef4d91234542222d709124eceaaa" 17 | good_url = GTMETRIX_REST_API_URL 18 | 19 | def set_known_good_settings(): 20 | """Get known good settings so we have a valid starting point for tests""" 21 | global GTMETRIX_REST_API_KEY, \ 22 | GTMETRIX_REST_API_EMAIL, \ 23 | GTMETRIX_REST_API_URL 24 | 25 | GTMETRIX_REST_API_EMAIL = good_email 26 | GTMETRIX_REST_API_KEY = good_apikey 27 | GTMETRIX_REST_API_URL = good_url 28 | -------------------------------------------------------------------------------- /gtmetrix/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import six 4 | from gtmetrix import settings 5 | from gtmetrix.exceptions import (GTmetrixAPIKeyIsNone, 6 | GTmetrixEmailIsNone, 7 | GTmetrixEmailIsNotStringtype, 8 | GTmetrixInvalidEmail, 9 | GTmetrixAPIUrlIsNone, 10 | GTmetrixBadAPIUrl) 11 | 12 | __all__ = ['YSLOW_RULES', 13 | 'validate_email', 14 | 'validate_api_key', 15 | 'validate_api_url'] 16 | 17 | 18 | YSLOW_RULES = { 19 | 'ynumreq': { 20 | 'name': 'Make fewer HTTP requests', 21 | 'weight': 8 22 | }, 23 | 'ycdn': { 24 | 'name': 'Use a CDN', 25 | 'weight': 6 26 | }, 27 | 'yemptysrc': { 28 | 'name': 'Avoid empty src or href', 29 | 'weight': 30 30 | }, 31 | 'yexpires': { 32 | 'name': 'Add an Expires header', 33 | 'weight': 10 34 | }, 35 | 'ycompress': { 36 | 'name': 'Compress components', 37 | 'weight': 8 38 | }, 39 | 'ycsstop': { 40 | 'name': 'Put CSS at top', 41 | 'weight': 4 42 | }, 43 | 'yjsbottom': { 44 | 'name': 'Put Javascript at the bottom', 45 | 'weight': 4 46 | }, 47 | 'yexpressions': { 48 | 'name': 'Avoid CSS expression', 49 | 'weight': 3 50 | }, 51 | 'yexternal': { 52 | 'name': 'Make JS and CSS external', 53 | 'weight': 4 54 | }, 55 | 'ydns': { 56 | 'name': 'Reduce DNS lookups', 57 | 'weight': 3 58 | }, 59 | 'yminify': { 60 | 'name': 'Minify JS and CSS', 61 | 'weight': 4 62 | }, 63 | 'yredirects': { 64 | 'name': 'Avoid redirects', 65 | 'weight': 4 66 | }, 67 | 'ydupes': { 68 | 'name': 'Remove duplicate JS and CSS', 69 | 'weight': 4 70 | }, 71 | 'yetags': { 72 | 'name': 'Configure ETags', 73 | 'weight': 2 74 | }, 75 | 'yxhr': { 76 | 'name': 'Make Ajax cacheable', 77 | 'weight': 4 78 | }, 79 | 'yxhrmethod': { 80 | 'name': 'Use GET for AJAX requests', 81 | 'weight': 3 82 | }, 83 | 'ymindom': { 84 | 'name': 'Reduce the Number of DOM elements', 85 | 'weight': 3 86 | }, 87 | 'yno404': { 88 | 'name': 'No 404s', 89 | 'weight': 4 90 | }, 91 | 'ymincookie': { 92 | 'name': 'Reduce Cookie Size', 93 | 'weight': 3 94 | }, 95 | 'ycookiefree': { 96 | 'name': 'Use Cookie-free Domains', 97 | 'weight': 3 98 | }, 99 | 'ynofilter': { 100 | 'name': 'Avoid filters', 101 | 'weight': 4 102 | }, 103 | 'yimgnoscale': { 104 | 'name': 'Don\'t Scale Images in HTML', 105 | 'weight': 3 106 | }, 107 | 'yfavicon': { 108 | 'name': 'Make favicon Small and Cacheable', 109 | 'weight': 2 110 | } 111 | } 112 | 113 | # Snagged from: https://www.scottbrady91.com/Email-Verification/Python-Email-Verification-Script 114 | email_re = re.compile( 115 | '^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(''\.[a-z]{2,''4})$') 116 | 117 | 118 | def validate_email(email): 119 | """Check for valid email address, raise correct exception if not.""" 120 | if email is None: 121 | raise GTmetrixEmailIsNone 122 | 123 | if not isinstance(email, six.string_types): 124 | raise GTmetrixEmailIsNotStringtype 125 | 126 | if email_re.match(email) is None: 127 | raise GTmetrixInvalidEmail 128 | 129 | return True 130 | 131 | 132 | def validate_api_key(key): 133 | """Check for valid API key. Stubbed out for now.""" 134 | if key is None: 135 | raise GTmetrixAPIKeyIsNone 136 | return True 137 | 138 | 139 | def validate_api_url(url): 140 | """Ensure that it's set to what it was when settings.py was written. 141 | 142 | Prevents e.g. testing api/0.1 calls against api/1.0 if it ever comes 143 | out.""" 144 | if url is None: 145 | raise GTmetrixAPIUrlIsNone 146 | 147 | if url != settings.good_url: 148 | raise GTmetrixBadAPIUrl 149 | 150 | return True 151 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | addopts = --verbose -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.3 2 | coverage==4.3.4 3 | cycler==0.10.0 4 | matplotlib==2.0.0 5 | nose2==0.6.5 6 | numpy==1.12.1 7 | packaging==16.8 8 | pluggy==0.4.0 9 | py==1.4.33 10 | pyparsing==2.2.0 11 | pytest==3.0.7 12 | pytest-cov==2.4.0 13 | pytest-runner==2.11.1 14 | python-dateutil==2.6.0 15 | pytz==2016.10 16 | requests==2.20.0 17 | six==1.10.0 18 | tox==2.6.0 19 | virtualenv==15.1.0 20 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test = pytest -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | python-gtmetrix 3 | ----------------- 4 | 5 | A Python client library for GTmetrix REST API 6 | 7 | """ 8 | import sys 9 | 10 | from setuptools import setup 11 | 12 | 13 | setup( 14 | name='python-gtmetrix', 15 | version='0.2.3', 16 | url='https://github.com/aisayko/python-gtmetrix', 17 | license='MIT License', 18 | author='Alex Isayko', 19 | author_email='alex.isayko@gmail.com', 20 | description='A Python client library for GTmetrix REST API.', 21 | keywords='python gtmetrix performance pagespeed yslow', 22 | long_description=__doc__, 23 | packages=['gtmetrix',], 24 | include_package_data=True, 25 | zip_safe=False, 26 | platforms='any', 27 | setup_requires=['pytest-runner'], 28 | tests_require=['pytest', 'pytest-cov'], 29 | install_requires=['requests>=2.13.0','six>=1.10.0'], 30 | classifiers=[ 31 | 'Development Status :: 5 - Production/Stable', 32 | 'Environment :: Web Environment', 33 | 'Intended Audience :: Developers', 34 | 'License :: OSI Approved :: MIT License', 35 | 'Operating System :: OS Independent', 36 | 'Programming Language :: Python', 37 | 'Programming Language :: Python :: 2.7', 38 | 'Programming Language :: Python :: 3.4', 39 | 'Programming Language :: Python :: 3.5', 40 | 'Programming Language :: Python :: 3.6', 41 | 'Topic :: Software Development :: Libraries :: Python Modules', 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /tests/test_gtmetrix_interface.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | GTmetrix Python API 4 | 5 | Interface Initializer Tests 6 | 7 | Tests to make sure the initializer catches bad inputs. 8 | 9 | They still might not work (i.e. inactive API key, etc.), but they'll at least 10 | be valid values. 11 | 12 | TestGTmetrixInitializerCatchesBadData 13 | 14 | Runs with forced BAD values, test passes if it fails properly. 15 | """ 16 | from unittest import TestCase 17 | 18 | from pytest import raises 19 | 20 | from gtmetrix import settings 21 | from gtmetrix.exceptions import GTmetrixAPIKeyIsNone, GTmetrixEmailIsNone, \ 22 | GTmetrixInvalidEmail 23 | from gtmetrix.interface import GTmetrixInterface 24 | from gtmetrix.settings import set_known_good_settings 25 | from .utils import restore_settings, save_settings 26 | 27 | 28 | class TestGTmetrixInitializerCatchesBadData(TestCase): 29 | """ 30 | Tests GTMetrixInterface initializer with purposely bad data. 31 | 32 | This ensures that the class will throw correct exceptions on bad data. 33 | 34 | NOTE: The initializer throws exceptions in a particular order so we only 35 | test one bad chunk at a time so as not to care about the order of 36 | testing within the initializer itself. 37 | """ 38 | bad_emails = ['email has spaces@example.com', 39 | '@example.no.email.address.just.domain.com', 40 | 'an.email.with.no.domain.or.at.sign', 41 | 'an.email.with.no.domain.with.at.sign@', 42 | 'any.other.examples.which.should.fail.find.a.list', ] 43 | 44 | def setup_method(self, method): 45 | """`setup_method` is invoked for every test method.""" 46 | save_settings() 47 | set_known_good_settings() 48 | 49 | def teardown_method(self, method): 50 | """teardown_method is invoked after every test method.""" 51 | restore_settings() 52 | 53 | def test_initializer_with_bad_emails_passed(self): 54 | """Ensure correct exception is raised when invalid email specified.""" 55 | for bad_email in self.bad_emails: 56 | with raises(GTmetrixInvalidEmail): 57 | gt = GTmetrixInterface(bad_email) 58 | 59 | def test_initializer_with_bad_emails_default(self): 60 | """Ensure correct exception is raised for invalid email via 61 | settings.""" 62 | for bad_email in self.bad_emails: 63 | with raises(GTmetrixInvalidEmail): 64 | settings.GTMETRIX_REST_API_EMAIL = bad_email 65 | gt = GTmetrixInterface() 66 | 67 | def test_email_is_None(self): 68 | """Ensure correct exception is raised when email is None.""" 69 | settings.GTMETRIX_REST_API_EMAIL = None 70 | with raises(GTmetrixEmailIsNone): 71 | gt = GTmetrixInterface() 72 | 73 | def test_api_key_is_None(self): 74 | """Ensure correct exception is raised when API key is None.""" 75 | settings.GTMETRIX_REST_API_KEY = None 76 | with raises(GTmetrixAPIKeyIsNone): 77 | gt = GTmetrixInterface() 78 | -------------------------------------------------------------------------------- /tests/test_gtmetrix_settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | GTmetrix Python API 4 | 5 | Settings Tests 6 | 7 | Sets a good/bad value for each configuration validator and ensures that good 8 | values pass and that bad values raise correct exception. 9 | 10 | No API calls are actually made. 11 | 12 | This runs three tests: 13 | 14 | 1> TestGTmetrixSettingsSuccess 15 | 16 | Runs with forced GOOD values. Will fal if validation methods are 17 | not working properly. 18 | 19 | 2> TestGTmetrixSettingsFailure 20 | 21 | Runs with forced BAD values. Ensures correct exception is raised. 22 | """ 23 | 24 | from unittest import TestCase 25 | from pytest import raises 26 | 27 | from gtmetrix.exceptions import (GTmetrixAPIUrlIsNone, 28 | GTmetrixBadAPIUrl, 29 | GTmetrixAPIKeyIsNone, 30 | GTmetrixEmailIsNone, 31 | GTmetrixEmailIsNotStringtype) 32 | from gtmetrix import settings 33 | 34 | from gtmetrix.utils import (validate_email, 35 | validate_api_key, 36 | validate_api_url) 37 | 38 | from .utils import (save_settings, 39 | restore_settings) 40 | 41 | 42 | class TestGTmetrixSettingsSuccess(TestCase): 43 | """ 44 | Tests that things succeed when they're supposed to. 45 | 46 | Set each settings.* to valid values and make sure they pass validation. 47 | 48 | Saves/restores global settings on entry/exit. 49 | """ 50 | @classmethod 51 | def setUpClass(cls): 52 | """Ensure that our settings DO contain valid values.""" 53 | save_settings() 54 | settings.set_known_good_settings() 55 | 56 | def test_email_OK(self): 57 | """Ensure known good email passes.""" 58 | validate_email(settings.GTMETRIX_REST_API_EMAIL) 59 | 60 | def test_api_key_OK(self): 61 | """Ensure known good API key passes.""" 62 | validate_api_key(settings.GTMETRIX_REST_API_KEY) 63 | 64 | def test_url_OK(self): 65 | """Test whether URL is same as when tests were written.""" 66 | validate_api_url(settings.GTMETRIX_REST_API_URL) 67 | 68 | @classmethod 69 | def tearDownClass(cls): 70 | """Put settings back as they were before test.""" 71 | restore_settings() 72 | 73 | 74 | class TestGTmetrixSettingsFailure(TestCase): 75 | """ 76 | Tests that things fail when they're supposed to. 77 | 78 | Set each settings.* value to None, then call validators. 79 | 80 | TODO: Add any specific values that fail in practice to ensure they're 81 | caught. 82 | 83 | NOTE: These do not touch the global state and so don't save/restore it. 84 | """ 85 | def test_api_key_is_None(self): 86 | """Test failure for key is None.""" 87 | with raises(GTmetrixAPIKeyIsNone): 88 | validate_api_key(None) 89 | 90 | def test_email_is_None(self): 91 | """Test failure for email is None.""" 92 | with raises(GTmetrixEmailIsNone): 93 | validate_email(None) 94 | 95 | def test_email_is_not_stringtype(self): 96 | """Ensure that we catch non-string emails.""" 97 | with raises(GTmetrixEmailIsNotStringtype): 98 | validate_email(12345678) 99 | 100 | def test_url_is_None(self): 101 | """Test failure for api url is None.""" 102 | with raises(GTmetrixAPIUrlIsNone): 103 | validate_api_url(None) 104 | 105 | def test_url_is_wrong(self): 106 | """Test failure for api url is not the same as when tests written.""" 107 | with raises(GTmetrixBadAPIUrl): 108 | validate_api_url("this is not the URL you are looking for...") 109 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Utilities to support testing.""" 3 | from gtmetrix import settings 4 | 5 | G_saved_settings = None 6 | 7 | # Change this when the test URL changes 8 | G_test_valid_url = 'https://gtmetrix.com/api/0.1/test' 9 | 10 | def save_settings(): 11 | global G_saved_settings 12 | G_saved_settings = (settings.GTMETRIX_REST_API_EMAIL, 13 | settings.GTMETRIX_REST_API_KEY, 14 | settings.GTMETRIX_REST_API_URL) 15 | 16 | 17 | def restore_settings(): 18 | settings.GTMETRIX_REST_API_EMAIL, \ 19 | settings.GTMETRIX_REST_API_KEY, \ 20 | settings.GTMETRIX_REST_API_URL = G_saved_settings 21 | 22 | 23 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py35,py36 3 | 4 | [testenv] 5 | deps = pytest 6 | commands = python setup.py test 7 | --------------------------------------------------------------------------------