From 5cace5c4846d71393896cb63085dabc0bf74ccf3 Mon Sep 17 00:00:00 2001 From: "Md. Almas Ali" Date: Wed, 31 Jan 2024 02:29:00 +0600 Subject: [PATCH 01/11] perf: moved to package - separaterd sync and async. --- spyip/backend.py | 177 ----------------------------------------------- 1 file changed, 177 deletions(-) delete mode 100644 spyip/backend.py diff --git a/spyip/backend.py b/spyip/backend.py deleted file mode 100644 index 80a7f15..0000000 --- a/spyip/backend.py +++ /dev/null @@ -1,177 +0,0 @@ -from typing import List, Union -import asyncio -import random -import string - -import httpx - -from .exceptions import ( - TooManyRequests, - ConnectionTimeout, - StatusError, -) -from .models import ( - IPResponse, - DNSResponse, -) - - -def get_random_string(length: int = 32) -> str: - """Generate a random string of fixed length.""" - letters = string.ascii_lowercase + string.digits - return ''.join(random.sample(letters, length)) - - -# API endpoints for IP address lookup -trace_me_url = 'http://ip-api.com/json/' -trace_ip_url = 'http://ip-api.com/json/%(query)s' -trace_dns_url = f'http://{get_random_string(32)}.edns.ip-api.com/json/' -trace_ip_batch_url = 'http://ip-api.com/batch' - -headers = { - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', - 'Accept-Encoding': 'gzip, deflate', - 'Accept-Language': 'en-US,en;q=0.5', - 'Connection': 'keep-alive', - 'Upgrade-Insecure-Requests': '1', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0', -} - - -def trace_me( - timeout: int = 5, - lang: str = 'en', -) -> Union[IPResponse, None]: - """Trace your own IP address.""" - try: - res = httpx.get( - url=trace_me_url, - params={'fields': 66842623, 'lang': lang}, - headers=headers, - timeout=timeout, - ) - data = res.json() - data['as_'] = data.pop('as') - - if res.status_code == 200: - return IPResponse(**data) - else: - raise StatusError(f'Invalid status code: {res.status_code}. Expected 200.') - - # 408 Request Timeout - except httpx._exceptions.ConnectTimeout: - raise ConnectionTimeout( - 'Connection timeout. The server timed out waiting for the request. According to the HTTP specification, the client is allowed to repeat the request again after some time.' - ) - # 429 Too Many Requests - except httpx._exceptions.TooManyRedirects: - raise TooManyRequests( - 'Too many requests. Our endpoints are limited to 45 HTTP requests per minute from an IP address. If you go over this limit your requests will be throttled (HTTP 429) until your rate limit window is reset.' - ) - - -def trace_ip( - query: str, - timeout: int = 5, - lang: str = 'en', -) -> IPResponse: - """Trace IP address""" - try: - res = httpx.get( - url=trace_ip_url % {'query': query}, - params={'fields': 66842623, 'lang': lang}, - headers=headers, - timeout=timeout, - ) - data = res.json() - data['as_'] = data.pop('as') - - if res.status_code == 200: - return IPResponse(**data) - else: - raise StatusError(f'Invalid status code: {res.status_code}. Expected 200.') - - # 408 Request Timeout - except httpx._exceptions.ConnectTimeout: - raise ConnectionTimeout('The server timed out waiting for the request.') - # 429 Too Many Requests - except httpx._exceptions.TooManyRedirects: - raise TooManyRequests( - 'Too many requests. Our endpoints are limited to 45 HTTP requests per minute from an IP address. If you go over this limit your requests will be throttled (HTTP 429) until your rate limit window is reset.' - ) - - -def trace_dns( - timeout: int = 5, - lang: str = 'en', -) -> IPResponse: - """Trace your own DNS address.""" - try: - res = httpx.get( - url=trace_dns_url, - params={'fields': 66842623, 'lang': lang}, - headers=headers, - timeout=timeout, - ) - if res.status_code == 200: - return DNSResponse(**res.json()['dns']) - else: - raise StatusError(f'Invalid status code: {res.status_code}. Expected 200.') - # 408 Request Timeout - except httpx._exceptions.ConnectTimeout: - raise ConnectionTimeout( - 'Connection timeout. The server timed out waiting for the request. According to the HTTP specification, the client is allowed to repeat the request again after some time.' - ) - - # 429 Too Many Requests - except httpx._exceptions.TooManyRedirects: - raise TooManyRequests( - """\ -This endpoint is limited to 15 requests per minute from an IP address. - -If you go over the limit your requests will be throttled (HTTP 429) until your rate limit window is reset. If you constantly go over the limit your IP address will be banned for 1 hour. - -The returned HTTP header X-Rl contains the number of requests remaining in the current rate limit window. X-Ttl contains the seconds until the limit is reset. -Your implementation should always check the value of the X-Rl header, and if its is 0 you must not send any more requests for the duration of X-Ttl in seconds.""" - ) - - -def trace_ip_batch( - query_list: List[str], - timeout: int = 5, - lang: str = 'en', -) -> List[IPResponse]: - """Trace multiple IP addresses""" - try: - res = httpx.post( - url=trace_ip_batch_url, - params={'fields': 66842623, 'lang': lang}, - headers=headers, - timeout=timeout, - json=query_list, - ) - response = [] - if res.status_code == 200: - for x in res.json(): - x['as_'] = x.pop('as') - response.append(IPResponse(**x)) - return response - else: - raise StatusError(f'Invalid status code: {res.status_code}. Expected 200.') - # 408 Request Timeout - except httpx._exceptions.ConnectTimeout: - raise ConnectionTimeout( - 'Connection timeout. The server timed out waiting for the request. According to the HTTP specification, the client is allowed to repeat the request again after some time.' - ) - - # 429 Too Many Requests - except httpx._exceptions.TooManyRedirects: - raise TooManyRequests( - """\ -This endpoint is limited to 15 requests per minute from an IP address. - -If you go over the limit your requests will be throttled (HTTP 429) until your rate limit window is reset. If you constantly go over the limit your IP address will be banned for 1 hour. - -The returned HTTP header X-Rl contains the number of requests remaining in the current rate limit window. X-Ttl contains the seconds until the limit is reset. -Your implementation should always check the value of the X-Rl header, and if its is 0 you must not send any more requests for the duration of X-Ttl in seconds.""" - ) From 68b01bf88a460fb0b30a9fab7e9df75fb8b0947c Mon Sep 17 00:00:00 2001 From: "Md. Almas Ali" Date: Wed, 31 Jan 2024 02:30:02 +0600 Subject: [PATCH 02/11] feat: version upgrade and minor changes in keywords, descriptions. --- pyproject.toml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a3f0948..6d842e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ dependencies = [ requires-python = ">=3.7" authors = [{name = "Md. Almas Ali", email = "almaspr3@gmail.com"}] maintainers = [{name = "Md. Almas Ali", email = "almaspr3@gmail.com"}] -description = "A simple IP lookup tool written in Python." +description = "A simple IP lookup tool written in Python with concurrency support." readme = "README.md" license = {file = "LICENSE"} keywords = [ @@ -16,6 +16,17 @@ keywords = [ "lookup", "utility", "simple", + "python", + "ipv6", + "ipv4", + "ip-lookup", + "ip-location", + "ip-geolocation", + "python-package", + "ip-info", + "ip-information", + "ip-info-api", + "ip-info-api-python", ] classifiers = [ "Topic :: Internet", From e741c9af06f4d993cd10a3dc647eaa5209dc75d3 Mon Sep 17 00:00:00 2001 From: "Md. Almas Ali" Date: Wed, 31 Jan 2024 02:31:24 +0600 Subject: [PATCH 03/11] feat: changed packages for new concurrent modules, upgraded version. --- spyip/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/spyip/__init__.py b/spyip/__init__.py index d86ca09..6c82f7e 100644 --- a/spyip/__init__.py +++ b/spyip/__init__.py @@ -1,10 +1,10 @@ """ -SpyIP - A simple IP lookup tool written in Python. +SpyIP - A simple IP lookup tool written in Python with concurrency support. Dependent on: ------------- httpx - pydantic + attrs https://ip-api.com/ @@ -51,14 +51,16 @@ """ -from .backend import trace_me, trace_ip, trace_dns, trace_ip_batch +from .backends.synchronous import trace_me, trace_ip, trace_dns, trace_ip_batch +from .backends import asynchronous, synchronous - -__version__ = '0.2.0' +__version__ = '0.3.0' __author__ = 'Md. Almas Ali' __all__ = [ 'trace_me', 'trace_ip', 'trace_dns', 'trace_ip_batch', + 'asynchronous', + 'synchronous', ] From 369cdb5f95693db95ed3503588873ff8c4fd1f90 Mon Sep 17 00:00:00 2001 From: "Md. Almas Ali" Date: Wed, 31 Jan 2024 02:31:55 +0600 Subject: [PATCH 04/11] style: description change. --- spyip/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spyip/exceptions.py b/spyip/exceptions.py index deb537f..0558b7b 100644 --- a/spyip/exceptions.py +++ b/spyip/exceptions.py @@ -1,5 +1,5 @@ """ -All exceptions used in the spyip package +SpyIP Exceptions. """ From c0cd75354a234816ddc52717b374a44797c81598 Mon Sep 17 00:00:00 2001 From: "Md. Almas Ali" Date: Wed, 31 Jan 2024 02:32:15 +0600 Subject: [PATCH 05/11] style: description change. --- spyip/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spyip/models.py b/spyip/models.py index 8539f69..792abc4 100644 --- a/spyip/models.py +++ b/spyip/models.py @@ -1,3 +1,6 @@ +""" +SpyIP response models. +""" from attr import define, field, asdict import json From 36af730778999d54bf01128cc2e9551928e2dff8 Mon Sep 17 00:00:00 2001 From: "Md. Almas Ali" Date: Wed, 31 Jan 2024 02:33:47 +0600 Subject: [PATCH 06/11] refactor: moved all functions to synchronous module. Will be use as default import. --- spyip/backends/synchronous.py | 177 ++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 spyip/backends/synchronous.py diff --git a/spyip/backends/synchronous.py b/spyip/backends/synchronous.py new file mode 100644 index 0000000..6bc20f7 --- /dev/null +++ b/spyip/backends/synchronous.py @@ -0,0 +1,177 @@ +from typing import List, Union +import asyncio +import random +import string + +import httpx + +from ..exceptions import ( + TooManyRequests, + ConnectionTimeout, + StatusError, +) +from ..models import ( + IPResponse, + DNSResponse, +) + + +def get_random_string(length: int = 32) -> str: + """Generate a random string of fixed length.""" + letters = string.ascii_lowercase + string.digits + return ''.join(random.sample(letters, length)) + + +# API endpoints for IP address lookup +trace_me_url = 'http://ip-api.com/json/' +trace_ip_url = 'http://ip-api.com/json/%(query)s' +trace_dns_url = f'http://{get_random_string(32)}.edns.ip-api.com/json/' +trace_ip_batch_url = 'http://ip-api.com/batch' + +headers = { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'en-US,en;q=0.5', + 'Connection': 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0', +} + + +def trace_me( + timeout: int = 5, + lang: str = 'en', +) -> Union[IPResponse, None]: + """Trace your own IP address.""" + try: + res = httpx.get( + url=trace_me_url, + params={'fields': 66842623, 'lang': lang}, + headers=headers, + timeout=timeout, + ) + data = res.json() + data['as_'] = data.pop('as') + + if res.status_code == 200: + return IPResponse(**data) + else: + raise StatusError(f'Invalid status code: {res.status_code}. Expected 200.') + + # 408 Request Timeout + except httpx._exceptions.ConnectTimeout: + raise ConnectionTimeout( + 'Connection timeout. The server timed out waiting for the request. According to the HTTP specification, the client is allowed to repeat the request again after some time.' + ) + # 429 Too Many Requests + except httpx._exceptions.TooManyRedirects: + raise TooManyRequests( + 'Too many requests. Our endpoints are limited to 45 HTTP requests per minute from an IP address. If you go over this limit your requests will be throttled (HTTP 429) until your rate limit window is reset.' + ) + + +def trace_ip( + query: str, + timeout: int = 5, + lang: str = 'en', +) -> IPResponse: + """Trace IP address""" + try: + res = httpx.get( + url=trace_ip_url % {'query': query}, + params={'fields': 66842623, 'lang': lang}, + headers=headers, + timeout=timeout, + ) + data = res.json() + data['as_'] = data.pop('as') + + if res.status_code == 200: + return IPResponse(**data) + else: + raise StatusError(f'Invalid status code: {res.status_code}. Expected 200.') + + # 408 Request Timeout + except httpx._exceptions.ConnectTimeout: + raise ConnectionTimeout('The server timed out waiting for the request.') + # 429 Too Many Requests + except httpx._exceptions.TooManyRedirects: + raise TooManyRequests( + 'Too many requests. Our endpoints are limited to 45 HTTP requests per minute from an IP address. If you go over this limit your requests will be throttled (HTTP 429) until your rate limit window is reset.' + ) + + +def trace_dns( + timeout: int = 5, + lang: str = 'en', +) -> IPResponse: + """Trace your own DNS address.""" + try: + res = httpx.get( + url=trace_dns_url, + params={'fields': 66842623, 'lang': lang}, + headers=headers, + timeout=timeout, + ) + if res.status_code == 200: + return DNSResponse(**res.json()['dns']) + else: + raise StatusError(f'Invalid status code: {res.status_code}. Expected 200.') + # 408 Request Timeout + except httpx._exceptions.ConnectTimeout: + raise ConnectionTimeout( + 'Connection timeout. The server timed out waiting for the request. According to the HTTP specification, the client is allowed to repeat the request again after some time.' + ) + + # 429 Too Many Requests + except httpx._exceptions.TooManyRedirects: + raise TooManyRequests( + """\ +This endpoint is limited to 15 requests per minute from an IP address. + +If you go over the limit your requests will be throttled (HTTP 429) until your rate limit window is reset. If you constantly go over the limit your IP address will be banned for 1 hour. + +The returned HTTP header X-Rl contains the number of requests remaining in the current rate limit window. X-Ttl contains the seconds until the limit is reset. +Your implementation should always check the value of the X-Rl header, and if its is 0 you must not send any more requests for the duration of X-Ttl in seconds.""" + ) + + +def trace_ip_batch( + query_list: List[str], + timeout: int = 5, + lang: str = 'en', +) -> List[IPResponse]: + """Trace multiple IP addresses""" + try: + res = httpx.post( + url=trace_ip_batch_url, + params={'fields': 66842623, 'lang': lang}, + headers=headers, + timeout=timeout, + json=query_list, + ) + response = [] + if res.status_code == 200: + for x in res.json(): + x['as_'] = x.pop('as') + response.append(IPResponse(**x)) + return response + else: + raise StatusError(f'Invalid status code: {res.status_code}. Expected 200.') + # 408 Request Timeout + except httpx._exceptions.ConnectTimeout: + raise ConnectionTimeout( + 'Connection timeout. The server timed out waiting for the request. According to the HTTP specification, the client is allowed to repeat the request again after some time.' + ) + + # 429 Too Many Requests + except httpx._exceptions.TooManyRedirects: + raise TooManyRequests( + """\ +This endpoint is limited to 15 requests per minute from an IP address. + +If you go over the limit your requests will be throttled (HTTP 429) until your rate limit window is reset. If you constantly go over the limit your IP address will be banned for 1 hour. + +The returned HTTP header X-Rl contains the number of requests remaining in the current rate limit window. X-Ttl contains the seconds until the limit is reset. +Your implementation should always check the value of the X-Rl header, and if its is 0 you must not send any more requests for the duration of X-Ttl in seconds.""" + ) From 7e42debba8f4a4641311763847e96166fc96c0e8 Mon Sep 17 00:00:00 2001 From: "Md. Almas Ali" Date: Wed, 31 Jan 2024 02:34:55 +0600 Subject: [PATCH 07/11] feat: concurrency supports in asynchronous backend. --- spyip/backends/asynchronous.py | 189 +++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 spyip/backends/asynchronous.py diff --git a/spyip/backends/asynchronous.py b/spyip/backends/asynchronous.py new file mode 100644 index 0000000..ace45bb --- /dev/null +++ b/spyip/backends/asynchronous.py @@ -0,0 +1,189 @@ +from typing import List, Union +import asyncio +import random +import string + +import httpx + +from spyip.exceptions import ( + TooManyRequests, + ConnectionTimeout, + StatusError, +) +from spyip.models import ( + IPResponse, + DNSResponse, +) + + +def get_random_string(length: int = 32) -> str: + """Generate a random string of fixed length.""" + letters = string.ascii_lowercase + string.digits + return ''.join(random.sample(letters, length)) + + +# API endpoints for IP address lookup +trace_me_url = 'http://ip-api.com/json/' +trace_ip_url = 'http://ip-api.com/json/%(query)s' +trace_dns_url = f'http://{get_random_string(32)}.edns.ip-api.com/json/' +trace_ip_batch_url = 'http://ip-api.com/batch' + +headers = { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Encoding': 'gzip, deflate', + 'Accept-Language': 'en-US,en;q=0.5', + 'Connection': 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0', +} + + +async def trace_me( + timeout: int = 5, + lang: str = 'en', +) -> Union[IPResponse, None]: + """Trace your own IP address.""" + async with httpx.AsyncClient() as client: + try: + res = await client.get( + url=trace_me_url, + params={'fields': 66842623, 'lang': lang}, + headers=headers, + timeout=timeout, + ) + data = res.json() + data['as_'] = data.pop('as') + + if res.status_code == 200: + return IPResponse(**data) + else: + raise StatusError( + f'Invalid status code: {res.status_code}. Expected 200.' + ) + + # 408 Request Timeout + except httpx.ConnectTimeout: + raise ConnectionTimeout( + 'Connection timeout. The server timed out waiting for the request. According to the HTTP specification, the client is allowed to repeat the request again after some time.' + ) + # 429 Too Many Requests + except httpx.TooManyRedirects: + raise TooManyRequests( + 'Too many requests. Our endpoints are limited to 45 HTTP requests per minute from an IP address. If you go over this limit your requests will be throttled (HTTP 429) until your rate limit window is reset.' + ) + + +async def trace_ip( + query: str, + timeout: int = 5, + lang: str = 'en', +) -> IPResponse: + """Trace IP address""" + async with httpx.AsyncClient() as client: + try: + res = await client.get( + url=trace_ip_url % {'query': query}, + params={'fields': 66842623, 'lang': lang}, + headers=headers, + timeout=timeout, + ) + data = res.json() + data['as_'] = data.pop('as') + + if res.status_code == 200: + return IPResponse(**data) + else: + raise StatusError( + f'Invalid status code: {res.status_code}. Expected 200.' + ) + + # 408 Request Timeout + except httpx.ConnectTimeout: + raise ConnectionTimeout('The server timed out waiting for the request.') + # 429 Too Many Requests + except httpx.TooManyRedirects: + raise TooManyRequests( + 'Too many requests. Our endpoints are limited to 45 HTTP requests per minute from an IP address. If you go over this limit your requests will be throttled (HTTP 429) until your rate limit window is reset.' + ) + + +async def trace_dns( + timeout: int = 5, + lang: str = 'en', +) -> IPResponse: + """Trace your own DNS address.""" + async with httpx.AsyncClient() as client: + try: + res = await client.get( + url=trace_dns_url, + params={'fields': 66842623, 'lang': lang}, + headers=headers, + timeout=timeout, + ) + if res.status_code == 200: + return DNSResponse(**res.json()['dns']) + else: + raise StatusError( + f'Invalid status code: {res.status_code}. Expected 200.' + ) + # 408 Request Timeout + except httpx.ConnectTimeout: + raise ConnectionTimeout( + 'Connection timeout. The server timed out waiting for the request. According to the HTTP specification, the client is allowed to repeat the request again after some time.' + ) + + # 429 Too Many Requests + except httpx.TooManyRedirects: + raise TooManyRequests( + """\ + This endpoint is limited to 15 requests per minute from an IP address. + + If you go over the limit your requests will be throttled (HTTP 429) until your rate limit window is reset. If you constantly go over the limit your IP address will be banned for 1 hour. + + The returned HTTP header X-Rl contains the number of requests remaining in the current rate limit window. X-Ttl contains the seconds until the limit is reset. + Your implementation should always check the value of the X-Rl header, and if its is 0 you must not send any more requests for the duration of X-Ttl in seconds.""" + ) + + +async def trace_ip_batch( + query_list: List[str], + timeout: int = 5, + lang: str = 'en', +) -> List[IPResponse]: + """Trace multiple IP addresses""" + async with httpx.AsyncClient() as client: + try: + res = await client.post( + url=trace_ip_batch_url, + params={'fields': 66842623, 'lang': lang}, + headers=headers, + timeout=timeout, + json=query_list, + ) + response = [] + if res.status_code == 200: + for x in res.json(): + x['as_'] = x.pop('as') + response.append(IPResponse(**x)) + return response + else: + raise StatusError( + f'Invalid status code: {res.status_code}. Expected 200.' + ) + # 408 Request Timeout + except httpx.ConnectTimeout: + raise ConnectionTimeout( + 'Connection timeout. The server timed out waiting for the request. According to the HTTP specification, the client is allowed to repeat the request again after some time.' + ) + + # 429 Too Many Requests + except httpx.TooManyRedirects: + raise TooManyRequests( + """\ + This endpoint is limited to 15 requests per minute from an IP address. + + If you go over the limit your requests will be throttled (HTTP 429) until your rate limit window is reset. If you constantly go over the limit your IP address will be banned for 1 hour. + + The returned HTTP header X-Rl contains the number of requests remaining in the current rate limit window. X-Ttl contains the seconds until the limit is reset. + Your implementation should always check the value of the X-Rl header, and if its is 0 you must not send any more requests for the duration of X-Ttl in seconds.""" + ) From 05ca109bcdccebde64a4a6c673d98eb89ba06492 Mon Sep 17 00:00:00 2001 From: "Md. Almas Ali" Date: Wed, 31 Jan 2024 02:35:32 +0600 Subject: [PATCH 08/11] refactor: moved to tests/test_synchronous.py --- tests/test_backend.py | 90 ------------------------------------------- 1 file changed, 90 deletions(-) delete mode 100644 tests/test_backend.py diff --git a/tests/test_backend.py b/tests/test_backend.py deleted file mode 100644 index 05310c3..0000000 --- a/tests/test_backend.py +++ /dev/null @@ -1,90 +0,0 @@ -import unittest - -from spyip import trace_me, trace_dns, trace_ip, trace_ip_batch - - -class TestSpyIP(unittest.TestCase): - def test_trace_me(self): - self.assertEqual(trace_me().status, 'success') - - def test_trace_dns(self): - self.assertNotEqual(trace_dns().ip, '') - self.assertNotEqual(trace_dns().geo, '') - - def test_trace_ip(self): - self.assertEqual(trace_ip(query='31.13.64.35').status, 'success') - - def test_trace_ip_batch(self): - """Check all status is success or not""" - res = trace_ip_batch( - query_list=[ - '31.13.64.35', # facebook.com - '142.250.193.206', # google.com - '20.205.243.166', # github.com - '20.236.44.162', # microsoft.com - ] - ) - status_list = [i.status == 'success' for i in res] - self.assertTrue(all(status_list)) - - def test_ip_response(self): - """ - { - "status": "success", - "continent": "Asia", - "continentCode": "AS", - "country": "India", - "countryCode": "IN", - "region": "DL", - "regionName": "National Capital Territory of Delhi", - "city": "New Delhi", - "district": "", - "zip": "110001", - "lat": 28.6139, - "lon": 77.209, - "timezone": "Asia/Kolkata", - "offset": 19800, - "currency": "INR", - "isp": "Google LLC", - "org": "Google LLC", - "as": "AS15169 Google LLC", - "asname": "GOOGLE", - "mobile": false, - "proxy": false, - "hosting": true, - "query": "142.250.193.206", - } - """ - - res = trace_ip(query='142.250.193.206') - - self.assertEqual(res.status, 'success') - self.assertEqual(res.continent, 'Asia') - self.assertEqual(res.continentCode, 'AS') - self.assertEqual(res.country, 'India') - self.assertEqual(res.countryCode, 'IN') - self.assertEqual(res.region, 'DL') - self.assertEqual(res.regionName, 'National Capital Territory of Delhi') - self.assertEqual(res.city, 'New Delhi') - self.assertEqual(res.district, '') - self.assertEqual(res.zip_, '110001') - self.assertEqual(res.lat, 28.6139) - self.assertEqual(res.lon, 77.209) - self.assertEqual(res.timezone, 'Asia/Kolkata') - self.assertEqual(res.offset, 19800) - self.assertEqual(res.currency, 'INR') - self.assertEqual(res.isp, 'Google LLC') - self.assertEqual(res.org, 'Google LLC') - self.assertEqual(res.as_, 'AS15169 Google LLC') - self.assertEqual(res.asname, 'GOOGLE') - self.assertEqual(res.mobile, False) - self.assertEqual(res.proxy, False) - self.assertEqual(res.hosting, True) - self.assertEqual(res.query, '142.250.193.206') - - def test_json_output(self): - res = trace_ip(query='31.13.64.35') - self.assertEqual( - res.json(), - '{"status": "success", "continent": "Asia", "continentCode": "AS", "country": "India", "countryCode": "IN", "region": "WB", "regionName": "West Bengal", "city": "Kolkata", "district": "", "zip_": "700059", "lat": 22.518, "lon": 88.3832, "timezone": "Asia/Kolkata", "offset": 19800, "currency": "INR", "isp": "Facebook, Inc.", "org": "Meta Platforms Ireland Limited", "as_": "AS32934 Facebook, Inc.", "asname": "FACEBOOK", "mobile": false, "proxy": false, "hosting": false, "query": "31.13.64.35"}', - ) From a6b0e4c4a0e19e6c6914bd62b3affaa77127012b Mon Sep 17 00:00:00 2001 From: "Md. Almas Ali" Date: Wed, 31 Jan 2024 02:36:55 +0600 Subject: [PATCH 09/11] refactor: moved from tests/test_backend.py --- tests/test_synchronous.py | 94 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/test_synchronous.py diff --git a/tests/test_synchronous.py b/tests/test_synchronous.py new file mode 100644 index 0000000..2bb1f11 --- /dev/null +++ b/tests/test_synchronous.py @@ -0,0 +1,94 @@ +import unittest + +from spyip import trace_me, trace_dns, trace_ip, trace_ip_batch + + +class TestSpyIP(unittest.TestCase): + def test_trace_me(self): + self.assertEqual(trace_me().status, 'success') + + def test_trace_dns(self): + self.assertNotEqual(trace_dns().ip, '') + self.assertNotEqual(trace_dns().geo, '') + + def test_trace_ip(self): + self.assertEqual(trace_ip(query='31.13.64.35').status, 'success') + + def test_trace_ip_batch(self): + """Check all status is success or not""" + res = trace_ip_batch( + query_list=[ + '31.13.64.35', # facebook.com + '142.250.193.206', # google.com + '20.205.243.166', # github.com + '20.236.44.162', # microsoft.com + ] + ) + status_list = [i.status == 'success' for i in res] + self.assertTrue(all(status_list)) + + def test_ip_response(self): + """ + { + "status": "success", + "continent": "Asia", + "continentCode": "AS", + "country": "India", + "countryCode": "IN", + "region": "DL", + "regionName": "National Capital Territory of Delhi", + "city": "New Delhi", + "district": "", + "zip": "110001", + "lat": 28.6139, + "lon": 77.209, + "timezone": "Asia/Kolkata", + "offset": 19800, + "currency": "INR", + "isp": "Google LLC", + "org": "Google LLC", + "as": "AS15169 Google LLC", + "asname": "GOOGLE", + "mobile": false, + "proxy": false, + "hosting": true, + "query": "142.250.193.206", + } + """ + + res = trace_ip(query='142.250.193.206') + + self.assertEqual(res.status, 'success') + self.assertEqual(res.continent, 'Asia') + self.assertEqual(res.continentCode, 'AS') + self.assertEqual(res.country, 'India') + self.assertEqual(res.countryCode, 'IN') + self.assertEqual(res.region, 'DL') + self.assertEqual(res.regionName, 'National Capital Territory of Delhi') + self.assertEqual(res.city, 'New Delhi') + self.assertEqual(res.district, '') + self.assertEqual(res.zip_, '110001') + self.assertEqual(res.lat, 28.6139) + self.assertEqual(res.lon, 77.209) + self.assertEqual(res.timezone, 'Asia/Kolkata') + self.assertEqual(res.offset, 19800) + self.assertEqual(res.currency, 'INR') + self.assertEqual(res.isp, 'Google LLC') + self.assertEqual(res.org, 'Google LLC') + self.assertEqual(res.as_, 'AS15169 Google LLC') + self.assertEqual(res.asname, 'GOOGLE') + self.assertEqual(res.mobile, False) + self.assertEqual(res.proxy, False) + self.assertEqual(res.hosting, True) + self.assertEqual(res.query, '142.250.193.206') + + def test_json_output(self): + res = trace_ip(query='31.13.64.35') + self.assertEqual( + res.json(), + '{"status": "success", "continent": "Asia", "continentCode": "AS", "country": "India", "countryCode": "IN", "region": "WB", "regionName": "West Bengal", "city": "Kolkata", "district": "", "zip_": "700059", "lat": 22.518, "lon": 88.3832, "timezone": "Asia/Kolkata", "offset": 19800, "currency": "INR", "isp": "Facebook, Inc.", "org": "Meta Platforms Ireland Limited", "as_": "AS32934 Facebook, Inc.", "asname": "FACEBOOK", "mobile": false, "proxy": false, "hosting": false, "query": "31.13.64.35"}', + ) + + +if __name__ == '__main__': + unittest.main() From 00cc6ef156fcee02785c0c4932935c3691c525ed Mon Sep 17 00:00:00 2001 From: "Md. Almas Ali" Date: Wed, 31 Jan 2024 02:37:55 +0600 Subject: [PATCH 10/11] test: new tests for asynchronous backend. --- tests/test_asynchronous.py | 98 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 tests/test_asynchronous.py diff --git a/tests/test_asynchronous.py b/tests/test_asynchronous.py new file mode 100644 index 0000000..b2d698c --- /dev/null +++ b/tests/test_asynchronous.py @@ -0,0 +1,98 @@ +import unittest +import asyncio + +from spyip.backends.asynchronous import trace_me, trace_dns, trace_ip, trace_ip_batch + + +class TestSpyIP(unittest.IsolatedAsyncioTestCase): + async def test_trace_me(self): + response = await trace_me() + self.assertEqual(response.status, 'success') + + async def test_trace_dns(self): + response = await trace_dns() + self.assertNotEqual(response.ip, '') + self.assertNotEqual(response.geo, '') + + async def test_trace_ip(self): + response = await trace_ip(query='31.13.64.35') + self.assertEqual(response.status, 'success') + + async def test_trace_ip_batch(self): + """Check all status is success or not""" + res = await trace_ip_batch( + query_list=[ + '31.13.64.35', # facebook.com + '142.250.193.206', # google.com + '20.205.243.166', # github.com + '20.236.44.162', # microsoft.com + ] + ) + status_list = [i.status == 'success' for i in res] + self.assertTrue(all(status_list)) + + async def test_ip_response(self): + """ + { + "status": "success", + "continent": "Asia", + "continentCode": "AS", + "country": "India", + "countryCode": "IN", + "region": "DL", + "regionName": "National Capital Territory of Delhi", + "city": "New Delhi", + "district": "", + "zip": "110001", + "lat": 28.6139, + "lon": 77.209, + "timezone": "Asia/Kolkata", + "offset": 19800, + "currency": "INR", + "isp": "Google LLC", + "org": "Google LLC", + "as": "AS15169 Google LLC", + "asname": "GOOGLE", + "mobile": false, + "proxy": false, + "hosting": true, + "query": "142.250.193.206", + } + """ + + response = await trace_ip(query='142.250.193.206') + + self.assertEqual(response.status, 'success') + self.assertEqual(response.continent, 'Asia') + self.assertEqual(response.continentCode, 'AS') + self.assertEqual(response.country, 'India') + self.assertEqual(response.countryCode, 'IN') + self.assertEqual(response.region, 'DL') + self.assertEqual(response.regionName, 'National Capital Territory of Delhi') + self.assertEqual(response.city, 'New Delhi') + self.assertEqual(response.district, '') + self.assertEqual(response.zip_, '110001') + self.assertEqual(response.lat, 28.6139) + self.assertEqual(response.lon, 77.209) + self.assertEqual(response.timezone, 'Asia/Kolkata') + self.assertEqual(response.offset, 19800) + self.assertEqual(response.currency, 'INR') + self.assertEqual(response.isp, 'Google LLC') + self.assertEqual(response.org, 'Google LLC') + self.assertEqual(response.as_, 'AS15169 Google LLC') + self.assertEqual(response.asname, 'GOOGLE') + self.assertEqual(response.mobile, False) + self.assertEqual(response.proxy, False) + self.assertEqual(response.hosting, True) + self.assertEqual(response.query, '142.250.193.206') + + async def test_json_output(self): + response = await trace_ip(query='31.13.64.35') + self.assertEqual( + response.json(), + '{"status": "success", "continent": "Asia", "continentCode": "AS", "country": "India", "countryCode": "IN", "region": "WB", "regionName": "West Bengal", "city": "Kolkata", "district": "", "zip_": "700059", "lat": 22.518, "lon": 88.3832, "timezone": "Asia/Kolkata", "offset": 19800, "currency": "INR", "isp": "Facebook, Inc.", "org": "Meta Platforms Ireland Limited", "as_": "AS32934 Facebook, Inc.", "asname": "FACEBOOK", "mobile": false, "proxy": false, "hosting": false, "query": "31.13.64.35"}', + ) + + +if __name__ == '__main__': + asyncio.run(unittest.main()) From 432775b73aabb39555c8fa888c688fcd9a0e1ca2 Mon Sep 17 00:00:00 2001 From: "Md. Almas Ali" Date: Wed, 31 Jan 2024 02:39:34 +0600 Subject: [PATCH 11/11] docs: latest docs upgrades. --- README.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 96a1e4c..a361b13 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,33 @@

SpyIP

+

A simple IP lookup tool written in Python with concurrency support. +

+

+ + wakatime

-

A simple IP lookup tool written in Python. -

+## Table of Contents + +- [Introduction](#introduction) +- [Features](#features) +- [Installation](#installation) +- [Usage](#usage) +- [Localizations](#localizations) +- [Responses](#responses) +- [Exceptions](#exceptions) +- [Tests](#tests) +- [Contributing](#contributing) +- [License](#license) -SpyIP uses ip-api API to trace IP addresses. SpyIP is type annotated, unit tested, PEP8 compliant, documented and optimized for performance. +## Introduction + +SpyIP uses ip-api API to trace IP addresses. SpyIP is default synchronous and supports asynchronous operations. It is type annotated, PEP8 compliant, documented and unit tested. ## Features @@ -22,6 +39,9 @@ SpyIP uses ip-apiip-api
  • @@ -190,7 +256,7 @@ class DNSResponse:
  • -In batch query, `trace_ip_batch` returns a list of `IPResponse` objects. You can just iterate over the list and use as you need. +In batch query, `trace_ip_batch` returns a list of `IPResponse` objects (`List[IPResponse]`). You can just iterate over the list and use as you need. ## Exceptions @@ -202,12 +268,29 @@ In batch query, `trace_ip_batch` returns a list of `IPResponse` objects. You can ## Tests -Test cases are located in `tests` directory. You can run tests with the following command: +Test cases are located in `tests` directory. You can run tests with the following commands: ```bash +# run all tests in tests directory at once python -m unittest discover -s tests + +# run asynchronous tests +python tests/test_asynchronous.py + +# run synchronous tests +python tests/test_synchronous.py ``` +### Average Time Comparisons: + +| Types | Tests | Time | +| ------------ | ----- | ------ | +| Total tests | 12 | 6.188s | +| Asynchronous | 6 | 2.751s | +| Synchronous | 6 | 2.968s | + +**Note:** This time comparison is based on network performance and hardware capabilities. I am using a very old low-configuration laptop and running on Python 3.11.7. This result may vary depending on your system resources. + ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. If you have any idea, suggestion or question, feel free to open an issue. Please make sure to update tests as appropriate.