From 51a0c28bcffcfee72798be5d6f08272fd6f8c71d Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 20:28:57 +0100 Subject: [PATCH 01/14] Working --- .github/workflows/ci.yml | 1 + app/__init__.py | 0 app/args.py | 30 ++++++++++++++++ app/ip_info.py | 25 +++++++++++++ app/main.py | 58 +++++++++++++++++++++++++++++-- app/print_table.py | 18 ++++++++++ app/query_normalisation.py | 26 ++++++++++++++ tests/__init__.py | 0 tests/test_ip_info.py | 25 +++++++++++++ tests/test_main.py | 18 ++++++++++ tests/test_query_normalisation.py | 34 ++++++++++++++++++ 11 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 app/__init__.py create mode 100644 app/args.py create mode 100644 app/ip_info.py create mode 100644 app/print_table.py create mode 100644 app/query_normalisation.py create mode 100644 tests/__init__.py create mode 100644 tests/test_ip_info.py create mode 100644 tests/test_query_normalisation.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f7495a..01ee9a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: - 'README.md' - 'LICENSE.md' - '.gitignore' + - 'renovate.json' pull_request: jobs: diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/args.py b/app/args.py new file mode 100644 index 0000000..db7fee8 --- /dev/null +++ b/app/args.py @@ -0,0 +1,30 @@ +#!/usr/local/bin/python3 + +import argparse + +from app.query_normalisation import get_public_ip + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Query information about an IP address or domain name." + ) + parser.add_argument( + "-q", + "--query", + help="IP/domain name to query (default: current public IP)", + default=get_public_ip(), + ) + parser.add_argument( + "-p", + "--prefixes", + help="show advertised prefixes", + action="store_true", + ) + parser.add_argument( + "-n", + "--noheader", + help="do not print header", + action="store_true", + ) + return parser.parse_args() diff --git a/app/ip_info.py b/app/ip_info.py new file mode 100644 index 0000000..1888465 --- /dev/null +++ b/app/ip_info.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +import ipaddress +import requests + + +def get_ip_information(ipv4_address: ipaddress.IPv4Address) -> dict: + api_endpoint = "http://ip-api.com/json/{}".format(ipv4_address) + resp = requests.get(api_endpoint).json() + return resp + + +def get_autonomous_system_number(as_info: str) -> str: + as_number = as_info.split(" ")[0] + return as_number + + +def get_prefix_information(autonomous_system: int) -> list: + api_endpoint = "https://api.hackertarget.com/aslookup/?q={}".format( + str(autonomous_system) + ) + resp = requests.get(api_endpoint).text + prefixes = resp.split("\n") + prefixes.pop(0) + return prefixes diff --git a/app/main.py b/app/main.py index a6053f8..c0f5d6f 100644 --- a/app/main.py +++ b/app/main.py @@ -1,8 +1,62 @@ #!/usr/local/bin/python3 +from app.args import parse_args +from app.print_table import print_table, generate_prefix_string +from app.query_normalisation import is_ip_address, resolve_domain_name +from app.ip_info import ( + get_ip_information, + get_autonomous_system_number, + get_prefix_information, +) + + +HEADER = """----------------------------------------------- +| IP Address Information Lookup Tool (iPilot) | +| By Luke Tainton (@luketainton) | +-----------------------------------------------\n""" + + def main(): - # Commands here - + args = parse_args() + if not args.noheader: + print(HEADER) + + # Set IP to passed IP address, or resolve passed domain name to IPv4 + ip = ( + resolve_domain_name(args.query) if not is_ip_address(args.query) else args.query + ) + + # If not given an IPv4, and can't resolve to IPv4, then throw error and exit + if not ip: + print("ERROR: could not resolve query to IPv4 address.") + exit(1) + + # Get information from API + ip_info = get_ip_information(ip) + as_number = get_autonomous_system_number(ip_info.get("as")) + + # Assemble list for table generation + table_data = [ + ["IP Address", ip_info.get("query")], + ["Organization", ip_info.get("org")], + [ + "Location", + "{}/{}/{}".format( + ip_info.get("country"), ip_info.get("regionName"), ip_info.get("city") + ), + ], + ["Timezone", ip_info.get("timezone")], + ["Internet Service Provider", ip_info.get("isp")], + ["Autonomous System", as_number], + ] + + # If wanted, get prefix information + if args.prefixes: + prefix_info = get_prefix_information(as_number) + table_data.append(["Prefixes", generate_prefix_string(prefix_info)]) + + print_table(table_data) + if __name__ == "__main__": main() diff --git a/app/print_table.py b/app/print_table.py new file mode 100644 index 0000000..17d8e6d --- /dev/null +++ b/app/print_table.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +from tabulate import tabulate + + +def generate_prefix_string(prefixes: list) -> str: + n = 4 + try: + ret = "" + for i in range(0, len(prefixes), n): + ret += ", ".join(prefixes[i : i + n]) + "\n" + return ret + except AttributeError: + return None + + +def print_table(table_data) -> None: + print(tabulate(table_data)) diff --git a/app/query_normalisation.py b/app/query_normalisation.py new file mode 100644 index 0000000..68c24d7 --- /dev/null +++ b/app/query_normalisation.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +import ipaddress +import requests +import socket + + +def is_ip_address(query: str) -> bool: + try: + ipaddress.ip_address(query) + return True + except ValueError: + return False + + +def resolve_domain_name(domain_name: str) -> ipaddress.IPv4Address: + try: + ip = socket.gethostbyname(domain_name) + except socket.gaierror: + ip = None + return ip + + +def get_public_ip() -> ipaddress.IPv4Address: + ip = requests.get("https://api.ipify.org").text + return ip diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_ip_info.py b/tests/test_ip_info.py new file mode 100644 index 0000000..6c9cf1a --- /dev/null +++ b/tests/test_ip_info.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +from app.ip_info import ( + get_ip_information, + get_autonomous_system_number, + get_prefix_information, +) + + +def test_get_ip_information() -> None: + test_query = "1.2.3.4" + ip_info = get_ip_information(test_query) + assert ip_info.get("status") == "success" and ip_info.get("query") == test_query + + +def test_get_autonomous_system_number() -> None: + as_info = "AS5089 Virgin Media Limited" + as_number = get_autonomous_system_number(as_info) + assert as_number == "AS5089" + + +def test_get_prefix_information() -> None: + autonomous_system = "AS109" + prefixes = get_prefix_information(autonomous_system) + assert "144.254.0.0/16" in prefixes diff --git a/tests/test_main.py b/tests/test_main.py index e69de29..147f528 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +# from app.ip import is_ip_address + + +# def test_is_ip_address_true() -> None: +# test_query = "1.2.3.4" +# assert is_ip_address(test_query) + + +# def test_is_ip_address_false_ip() -> None: +# test_query = "256.315.16.23" +# assert not is_ip_address(test_query) + + +# def test_is_ip_address_false_fqdn() -> None: +# test_query = "google.com" +# assert not is_ip_address(test_query) diff --git a/tests/test_query_normalisation.py b/tests/test_query_normalisation.py new file mode 100644 index 0000000..6519a37 --- /dev/null +++ b/tests/test_query_normalisation.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +from app.query_normalisation import is_ip_address, resolve_domain_name, get_public_ip + + +def test_is_ip_address_true() -> None: + test_query = "1.2.3.4" + assert is_ip_address(test_query) + + +def test_is_ip_address_false_ip() -> None: + test_query = "256.315.16.23" + assert not is_ip_address(test_query) + + +def test_is_ip_address_false_fqdn() -> None: + test_query = "google.com" + assert not is_ip_address(test_query) + + +def test_resolve_domain_name_true() -> None: + domain_name = "one.one.one.one" + expected_results = ["1.1.1.1", "1.0.0.1"] # Could resolve to either IP + assert resolve_domain_name(domain_name) in expected_results + + +def test_resolve_domain_name_false() -> None: + domain_name = "hrrijoresdo.com" + assert not resolve_domain_name(domain_name) + + +def test_get_public_ip() -> None: + public_ip = get_public_ip() + assert is_ip_address(public_ip) -- 2.47.2 From b8b4a8be4bd8dfd3316f8eaaac159def38729e47 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 20:39:00 +0100 Subject: [PATCH 02/14] Add requirements and docstrings --- app/args.py | 1 + app/ip_info.py | 3 +++ app/main.py | 1 + app/print_table.py | 2 ++ app/query_normalisation.py | 3 +++ requirements.txt | 6 ++++++ tests/test_ip_info.py | 3 +++ tests/test_query_normalisation.py | 6 ++++++ 8 files changed, 25 insertions(+) diff --git a/app/args.py b/app/args.py index db7fee8..19db240 100644 --- a/app/args.py +++ b/app/args.py @@ -6,6 +6,7 @@ from app.query_normalisation import get_public_ip def parse_args() -> argparse.Namespace: + """Get arguments from user via the command line.""" parser = argparse.ArgumentParser( description="Query information about an IP address or domain name." ) diff --git a/app/ip_info.py b/app/ip_info.py index 1888465..589d28b 100644 --- a/app/ip_info.py +++ b/app/ip_info.py @@ -5,17 +5,20 @@ import requests def get_ip_information(ipv4_address: ipaddress.IPv4Address) -> dict: + """Retrieves information about a given IPv4 address from IP-API.com.""" api_endpoint = "http://ip-api.com/json/{}".format(ipv4_address) resp = requests.get(api_endpoint).json() return resp def get_autonomous_system_number(as_info: str) -> str: + """Parses AS number from provided AS information.""" as_number = as_info.split(" ")[0] return as_number def get_prefix_information(autonomous_system: int) -> list: + """Retrieves prefix information about a given autonomous system.""" api_endpoint = "https://api.hackertarget.com/aslookup/?q={}".format( str(autonomous_system) ) diff --git a/app/main.py b/app/main.py index c0f5d6f..ed65454 100644 --- a/app/main.py +++ b/app/main.py @@ -17,6 +17,7 @@ HEADER = """----------------------------------------------- def main(): + """Main function.""" args = parse_args() if not args.noheader: print(HEADER) diff --git a/app/print_table.py b/app/print_table.py index 17d8e6d..96fcdac 100644 --- a/app/print_table.py +++ b/app/print_table.py @@ -4,6 +4,7 @@ from tabulate import tabulate def generate_prefix_string(prefixes: list) -> str: + """Generate a string that spilts prefixes into rows of 5.""" n = 4 try: ret = "" @@ -15,4 +16,5 @@ def generate_prefix_string(prefixes: list) -> str: def print_table(table_data) -> None: + """Print table generated by tabulate.""" print(tabulate(table_data)) diff --git a/app/query_normalisation.py b/app/query_normalisation.py index 68c24d7..6ae9847 100644 --- a/app/query_normalisation.py +++ b/app/query_normalisation.py @@ -6,6 +6,7 @@ import socket def is_ip_address(query: str) -> bool: + """Verifies if a given query is a valid IPv4 address.""" try: ipaddress.ip_address(query) return True @@ -14,6 +15,7 @@ def is_ip_address(query: str) -> bool: def resolve_domain_name(domain_name: str) -> ipaddress.IPv4Address: + """Resolve a domain name via DNS or return None.""" try: ip = socket.gethostbyname(domain_name) except socket.gaierror: @@ -22,5 +24,6 @@ def resolve_domain_name(domain_name: str) -> ipaddress.IPv4Address: def get_public_ip() -> ipaddress.IPv4Address: + """Get the user's current public IPv4 address.""" ip = requests.get("https://api.ipify.org").text return ip diff --git a/requirements.txt b/requirements.txt index e69de29..cad61e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,6 @@ +certifi==2022.6.15 +charset-normalizer==2.0.12 +idna==3.3 +requests==2.28.0 +tabulate==0.8.10 +urllib3==1.26.9 diff --git a/tests/test_ip_info.py b/tests/test_ip_info.py index 6c9cf1a..a0645d5 100644 --- a/tests/test_ip_info.py +++ b/tests/test_ip_info.py @@ -8,18 +8,21 @@ from app.ip_info import ( def test_get_ip_information() -> None: + """TEST: ensure that the IP information API is working correctly.""" test_query = "1.2.3.4" ip_info = get_ip_information(test_query) assert ip_info.get("status") == "success" and ip_info.get("query") == test_query def test_get_autonomous_system_number() -> None: + """TEST: ensure that AS information is parsed into AS number correctly.""" as_info = "AS5089 Virgin Media Limited" as_number = get_autonomous_system_number(as_info) assert as_number == "AS5089" def test_get_prefix_information() -> None: + """TEST: ensure that advertised prefixes are retrieved correctly.""" autonomous_system = "AS109" prefixes = get_prefix_information(autonomous_system) assert "144.254.0.0/16" in prefixes diff --git a/tests/test_query_normalisation.py b/tests/test_query_normalisation.py index 6519a37..1d1f2c1 100644 --- a/tests/test_query_normalisation.py +++ b/tests/test_query_normalisation.py @@ -4,31 +4,37 @@ from app.query_normalisation import is_ip_address, resolve_domain_name, get_publ def test_is_ip_address_true() -> None: + """TEST: Verifies if a given query is a valid IPv4 address.""" test_query = "1.2.3.4" assert is_ip_address(test_query) def test_is_ip_address_false_ip() -> None: + """TEST: Verifies that None is returned if an invalid IP is given.""" test_query = "256.315.16.23" assert not is_ip_address(test_query) def test_is_ip_address_false_fqdn() -> None: + """TEST: Verifies that None is returned if a domain name is given.""" test_query = "google.com" assert not is_ip_address(test_query) def test_resolve_domain_name_true() -> None: + """TEST: Verifies that DNS resolution is working correctly.""" domain_name = "one.one.one.one" expected_results = ["1.1.1.1", "1.0.0.1"] # Could resolve to either IP assert resolve_domain_name(domain_name) in expected_results def test_resolve_domain_name_false() -> None: + """TEST: Verifiees that a non-existent domain is not resolved.""" domain_name = "hrrijoresdo.com" assert not resolve_domain_name(domain_name) def test_get_public_ip() -> None: + """TEST: Verifies that the current public IP is retrieved correctly.""" public_ip = get_public_ip() assert is_ip_address(public_ip) -- 2.47.2 From 50b6280e2ff58679a03d2bff1b1f5f0e696cdd60 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 20:50:11 +0100 Subject: [PATCH 03/14] Fix linting issues from CI #5 --- app/args.py | 2 +- app/ip_info.py | 6 ++---- app/main.py | 16 ++++++++-------- app/print_table.py | 8 ++++---- app/query_normalisation.py | 12 ++++++------ tests/test_ip_info.py | 2 +- tests/test_query_normalisation.py | 6 +++++- 7 files changed, 27 insertions(+), 25 deletions(-) diff --git a/app/args.py b/app/args.py index 19db240..120dfb1 100644 --- a/app/args.py +++ b/app/args.py @@ -1,4 +1,4 @@ -#!/usr/local/bin/python3 +#!/usr/local/env python3 import argparse diff --git a/app/ip_info.py b/app/ip_info.py index 589d28b..9a349ce 100644 --- a/app/ip_info.py +++ b/app/ip_info.py @@ -6,7 +6,7 @@ import requests def get_ip_information(ipv4_address: ipaddress.IPv4Address) -> dict: """Retrieves information about a given IPv4 address from IP-API.com.""" - api_endpoint = "http://ip-api.com/json/{}".format(ipv4_address) + api_endpoint = f"http://ip-api.com/json/{ipv4_address}" resp = requests.get(api_endpoint).json() return resp @@ -19,9 +19,7 @@ def get_autonomous_system_number(as_info: str) -> str: def get_prefix_information(autonomous_system: int) -> list: """Retrieves prefix information about a given autonomous system.""" - api_endpoint = "https://api.hackertarget.com/aslookup/?q={}".format( - str(autonomous_system) - ) + api_endpoint = f"https://api.hackertarget.com/aslookup/?q={str(autonomous_system)}" resp = requests.get(api_endpoint).text prefixes = resp.split("\n") prefixes.pop(0) diff --git a/app/main.py b/app/main.py index ed65454..1acf2de 100644 --- a/app/main.py +++ b/app/main.py @@ -1,9 +1,11 @@ #!/usr/local/bin/python3 +import sys + from app.args import parse_args from app.print_table import print_table, generate_prefix_string from app.query_normalisation import is_ip_address, resolve_domain_name -from app.ip_info import ( +from app.ip_info import ( # pragma: no cover get_ip_information, get_autonomous_system_number, get_prefix_information, @@ -23,17 +25,17 @@ def main(): print(HEADER) # Set IP to passed IP address, or resolve passed domain name to IPv4 - ip = ( + ip_address = ( resolve_domain_name(args.query) if not is_ip_address(args.query) else args.query ) # If not given an IPv4, and can't resolve to IPv4, then throw error and exit - if not ip: + if not ip_address: print("ERROR: could not resolve query to IPv4 address.") - exit(1) + sys.exit(1) # Get information from API - ip_info = get_ip_information(ip) + ip_info = get_ip_information(ip_address) as_number = get_autonomous_system_number(ip_info.get("as")) # Assemble list for table generation @@ -42,9 +44,7 @@ def main(): ["Organization", ip_info.get("org")], [ "Location", - "{}/{}/{}".format( - ip_info.get("country"), ip_info.get("regionName"), ip_info.get("city") - ), + f"{ip_info.get('country')}/{ip_info.get('regionName')}/{ip_info.get('city')}", ], ["Timezone", ip_info.get("timezone")], ["Internet Service Provider", ip_info.get("isp")], diff --git a/app/print_table.py b/app/print_table.py index 96fcdac..a5813b6 100644 --- a/app/print_table.py +++ b/app/print_table.py @@ -4,12 +4,12 @@ from tabulate import tabulate def generate_prefix_string(prefixes: list) -> str: - """Generate a string that spilts prefixes into rows of 5.""" - n = 4 + """Generate a string that spilts prefixes into rows of 4.""" + num_per_row = 4 try: ret = "" - for i in range(0, len(prefixes), n): - ret += ", ".join(prefixes[i : i + n]) + "\n" + for i in range(0, len(prefixes), num_per_row): + ret += ", ".join(prefixes[i : i + num_per_row]) + "\n" return ret except AttributeError: return None diff --git a/app/query_normalisation.py b/app/query_normalisation.py index 6ae9847..fe0cb31 100644 --- a/app/query_normalisation.py +++ b/app/query_normalisation.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 +import socket import ipaddress import requests -import socket def is_ip_address(query: str) -> bool: @@ -17,13 +17,13 @@ def is_ip_address(query: str) -> bool: def resolve_domain_name(domain_name: str) -> ipaddress.IPv4Address: """Resolve a domain name via DNS or return None.""" try: - ip = socket.gethostbyname(domain_name) + ip_address = socket.gethostbyname(domain_name) except socket.gaierror: - ip = None - return ip + ip_address = None + return ip_address def get_public_ip() -> ipaddress.IPv4Address: """Get the user's current public IPv4 address.""" - ip = requests.get("https://api.ipify.org").text - return ip + ip_address = requests.get("https://api.ipify.org").text + return ip_address diff --git a/tests/test_ip_info.py b/tests/test_ip_info.py index a0645d5..5b5d79b 100644 --- a/tests/test_ip_info.py +++ b/tests/test_ip_info.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -from app.ip_info import ( +from app.ip_info import ( # pragma: no cover get_ip_information, get_autonomous_system_number, get_prefix_information, diff --git a/tests/test_query_normalisation.py b/tests/test_query_normalisation.py index 1d1f2c1..4a58abd 100644 --- a/tests/test_query_normalisation.py +++ b/tests/test_query_normalisation.py @@ -1,6 +1,10 @@ #!/usr/bin/env python3 -from app.query_normalisation import is_ip_address, resolve_domain_name, get_public_ip +from app.query_normalisation import ( # pragma: no cover + is_ip_address, + resolve_domain_name, + get_public_ip, +) def test_is_ip_address_true() -> None: -- 2.47.2 From 460e74c6bb0be1004c2f930e8972291443c95cd6 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 20:54:10 +0100 Subject: [PATCH 04/14] Add module docstrings --- app/args.py | 2 ++ app/ip_info.py | 2 ++ app/main.py | 2 ++ app/print_table.py | 2 ++ app/query_normalisation.py | 2 ++ tests/test_ip_info.py | 2 ++ tests/test_main.py | 2 ++ tests/test_query_normalisation.py | 2 ++ 8 files changed, 16 insertions(+) diff --git a/app/args.py b/app/args.py index 120dfb1..400b448 100644 --- a/app/args.py +++ b/app/args.py @@ -1,5 +1,7 @@ #!/usr/local/env python3 +"""MODULE: Provides CLI arguments to the application.""" + import argparse from app.query_normalisation import get_public_ip diff --git a/app/ip_info.py b/app/ip_info.py index 9a349ce..ade9dde 100644 --- a/app/ip_info.py +++ b/app/ip_info.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +"""MODULE: Provides functions to call various APIs to retrieve IP/prefix information.""" + import ipaddress import requests diff --git a/app/main.py b/app/main.py index 1acf2de..898afb8 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,7 @@ #!/usr/local/bin/python3 +"""MODULE: Main application module.""" + import sys from app.args import parse_args diff --git a/app/print_table.py b/app/print_table.py index a5813b6..fd9f118 100644 --- a/app/print_table.py +++ b/app/print_table.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +"""MODULE: Provides functions for preparing, then printing, retrieved data.""" + from tabulate import tabulate diff --git a/app/query_normalisation.py b/app/query_normalisation.py index fe0cb31..5bf32fd 100644 --- a/app/query_normalisation.py +++ b/app/query_normalisation.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +"""MODULE: Provides functions that ensure an IP address is available to query the APIs for.""" + import socket import ipaddress import requests diff --git a/tests/test_ip_info.py b/tests/test_ip_info.py index 5b5d79b..ea84846 100644 --- a/tests/test_ip_info.py +++ b/tests/test_ip_info.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +"""MODULE: Provides test cases for app/ip_info.py.""" + from app.ip_info import ( # pragma: no cover get_ip_information, get_autonomous_system_number, diff --git a/tests/test_main.py b/tests/test_main.py index 147f528..ebbdd22 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +"""MODULE: Provides test cases for app/main.py.""" + # from app.ip import is_ip_address diff --git a/tests/test_query_normalisation.py b/tests/test_query_normalisation.py index 4a58abd..6fdd6a6 100644 --- a/tests/test_query_normalisation.py +++ b/tests/test_query_normalisation.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 +"""MODULE: Provides test cases for app/query_normalisation.py.""" + from app.query_normalisation import ( # pragma: no cover is_ip_address, resolve_domain_name, -- 2.47.2 From 7d9f74593083386716529a04562e609d314fed45 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 21:38:29 +0100 Subject: [PATCH 05/14] Add build and publish steps to CI --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01ee9a3..e2adacb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,3 +79,36 @@ jobs: run: coverage run -m py.test -v - name: Upload Coverage to Codecov uses: codecov/codecov-action@v3 + + build: + needs: + - skip_duplicate + - lint + - test + if: ${{ needs.skip_duplicate.outputs.should_skip == 'false' }} + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v3 + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: 3.10 + # - name: Install dependencies + # run: pip install -r requirements.txt && pip install -r requirements-dev.txt + - name: Install build dependencies + run: pip install setuptools wheel + - name: Build wheel file + run: python setup.py bdist_wheel + - id: skip_check + uses: actions/upload-artifact@v3 + with: + name: whl + path: dist/ + + publish: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} -- 2.47.2 From 2cb2bc21906c0845dfd48a363ff5657e2d6c3390 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 21:45:16 +0100 Subject: [PATCH 06/14] Create setup.py --- .github/workflows/ci.yml | 12 ++++++++---- app/_version.py | 5 +++++ setup.py | 18 ++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 app/_version.py create mode 100644 setup.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2adacb..4a15ec8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -108,7 +108,11 @@ jobs: publish: if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: pypa/gh-action-pypi-publish@release/v1 - with: - user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} + needs: build + steps: + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + \ No newline at end of file diff --git a/app/_version.py b/app/_version.py new file mode 100644 index 0000000..5f91a76 --- /dev/null +++ b/app/_version.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 + +"""MODULE: Specifies app version.""" + +VERSION = "0.1" diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..341fe27 --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +from setuptools import setup + +from app._version import VERSION + +setup( + name="ipilot", + version=VERSION, + description="IP Information Lookup Tool", + author="Luke Tainton", + author_email="luke@tainton.uk", + packages=["ipilot"], + install_requires=["requests"], + entry_points={ + "console_scripts": [ + "ipilot = app.main:main", + ], + }, +) -- 2.47.2 From ffacf84dc76fee329a30f8a44eedc50a0c214aa3 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 21:49:56 +0100 Subject: [PATCH 07/14] Fix CI --- .github/workflows/ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a15ec8..282a421 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ on: jobs: skip_duplicate: + name: Skip if duplicate run runs-on: ubuntu-latest outputs: should_skip: ${{ steps.skip_check.outputs.should_skip }} @@ -44,6 +45,7 @@ jobs: uses: github/codeql-action/analyze@v2 lint: + name: Lint needs: skip_duplicate if: ${{ needs.skip_duplicate.outputs.should_skip == 'false' }} runs-on: ubuntu-latest @@ -60,6 +62,7 @@ jobs: run: pylint --recursive=yes . test: + name: Run unit tests needs: skip_duplicate if: ${{ needs.skip_duplicate.outputs.should_skip == 'false' }} runs-on: ubuntu-latest @@ -81,11 +84,10 @@ jobs: uses: codecov/codecov-action@v3 build: + name: Build needs: - - skip_duplicate - lint - test - if: ${{ needs.skip_duplicate.outputs.should_skip == 'false' }} runs-on: ubuntu-latest steps: - name: Check out repository code @@ -107,12 +109,12 @@ jobs: path: dist/ publish: - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + name: Publish needs: build + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} steps: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} - \ No newline at end of file -- 2.47.2 From 33b59706ea7b923c323c0be11b4b86c0f37b6b96 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 21:54:37 +0100 Subject: [PATCH 08/14] Fix CI --- .github/workflows/ci.yml | 1 + README.md | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 282a421..395467e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -112,6 +112,7 @@ jobs: name: Publish needs: build if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest steps: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/README.md b/README.md index 86b71e3..15f746b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ -# template +# iPilot [![CI](https://github.com/luketainton/pypilot/actions/workflows/ci.yml/badge.svg)](https://github.com/luketainton/pypilot/actions/workflows/ci.yml) ## Description +IP Information Lookup Tool ## How to install +`pip install ipilot` ## How to use +`ipilot --help` -- 2.47.2 From 751572bb348fa9b2f6c7051bc214a468287b7b6c Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 21:56:08 +0100 Subject: [PATCH 09/14] Add docstring to setup.py --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 341fe27..5bd8f6d 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,7 @@ +#!/usr/bin/env python3 + +"""SETUP: Build application .whl file.""" + from setuptools import setup from app._version import VERSION -- 2.47.2 From 31435b51ec7177367a779629deef579529bb8be5 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 21:58:16 +0100 Subject: [PATCH 10/14] Change 3.10 to '3.10' --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 395467e..bfad360 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,7 +95,7 @@ jobs: - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: - python-version: 3.10 + python-version: "3.10" # - name: Install dependencies # run: pip install -r requirements.txt && pip install -r requirements-dev.txt - name: Install build dependencies -- 2.47.2 From 117470feb06106c995e1dcf5dcfc54ecd6ca7a1a Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 22:00:05 +0100 Subject: [PATCH 11/14] Correct package name in setup.py --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5bd8f6d..05259ec 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( description="IP Information Lookup Tool", author="Luke Tainton", author_email="luke@tainton.uk", - packages=["ipilot"], + packages=["app"], install_requires=["requests"], entry_points={ "console_scripts": [ -- 2.47.2 From 10bb540d3d26fa4991e051e390ed233e39fa9c03 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 22:08:48 +0100 Subject: [PATCH 12/14] setup.py: dynamically pull dependencies from requirements.txt --- setup.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 05259ec..c8ec7b3 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,13 @@ from setuptools import setup from app._version import VERSION + +dependencies = [] +with open("requirements.txt", "r") as dep_file: + for dep_line in dep_file.readlines(): + dependencies.append(dep_line.replace("\n", "")) + + setup( name="ipilot", version=VERSION, @@ -13,7 +20,7 @@ setup( author="Luke Tainton", author_email="luke@tainton.uk", packages=["app"], - install_requires=["requests"], + install_requires=dependencies, entry_points={ "console_scripts": [ "ipilot = app.main:main", -- 2.47.2 From ab2a739360bf5648b13f935d2e4031d426098cb5 Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 22:13:05 +0100 Subject: [PATCH 13/14] setup.py: specify requirements.txt encoding --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index c8ec7b3..255974b 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ from app._version import VERSION dependencies = [] -with open("requirements.txt", "r") as dep_file: +with open("requirements.txt", "r", encoding="ascii") as dep_file: for dep_line in dep_file.readlines(): dependencies.append(dep_line.replace("\n", "")) -- 2.47.2 From 3b003b154a5f9fc247d79e6743d7ec08b7405fcd Mon Sep 17 00:00:00 2001 From: Luke Tainton Date: Sat, 25 Jun 2022 22:19:38 +0100 Subject: [PATCH 14/14] Update version to 1.0 --- app/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/_version.py b/app/_version.py index 5f91a76..c51437b 100644 --- a/app/_version.py +++ b/app/_version.py @@ -2,4 +2,4 @@ """MODULE: Specifies app version.""" -VERSION = "0.1" +VERSION = "1.0" -- 2.47.2