urlprobe 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
urlprobe-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Andrew Vella
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, 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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,128 @@
1
+ Metadata-Version: 2.4
2
+ Name: urlprobe
3
+ Version: 0.1.0
4
+ Summary: A tool to request a specified `url` and return the response.
5
+ License-File: LICENSE
6
+ Author: Andrew Vella
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Requires-Dist: click (>=8.2.1,<9.0.0)
15
+ Requires-Dist: flask (>=3.0.0,<4.0.0)
16
+ Requires-Dist: requests (>=2.32.3,<3.0.0)
17
+ Requires-Dist: rich (>=14.0.0,<15.0.0)
18
+ Requires-Dist: toml (>=0.10.2,<0.11.0)
19
+ Description-Content-Type: text/markdown
20
+
21
+ # URL Probe
22
+
23
+ [![Test Status](https://github.com/av603/urlprobe/actions/workflows/test.yml/badge.svg)](https://github.com/av603/urlprobe/actions/workflows/test.yml)
24
+ [![Code Coverage](https://codecov.io/gh/av603/urlprobe/branch/main/graph/badge.svg)](https://codecov.io/gh/av603/urlprobe)
25
+
26
+ This tool helps test complex cloud network setups, like those with private services that have specific exit points. It's especially useful for checking if your cloud services can talk to each other within a virtual private cloud and those services can make requests to the outside world as expected.
27
+
28
+ **Use Case** Say you have a Cloud Load Balancer, two Cloud Run services (Service A and Service B) and a Cloud Subnetwork with a static egress IP. You have deployed this infrastructure and want to verify that the Cloud Load Balancer can access Service A, that Service A can access Service B and that requests from Service B appear from the subnetwork static egress IP.
29
+
30
+ ```
31
+ Internet
32
+ |
33
+ |
34
+ +-------+
35
+ | |
36
+ | Cloud |
37
+ | Run | (A) - Publicly Accessible
38
+ +-------+
39
+ | (Target URL Parameter)
40
+ |
41
+ v (Internal Network Traffic)
42
+ +-------+
43
+ | |
44
+ | Cloud |
45
+ | Run | (B) - Internal Service
46
+ +-------+
47
+ | (Outbound Request via Static IP)
48
+ |
49
+ v
50
+ +---------------+
51
+ | External |
52
+ | Target Server |
53
+ +---------------+
54
+ ```
55
+
56
+ The network connectivity and the public IP can be verified by deploying the `urlprobe` tool to Service A and Service B and sending a chained `curl` request to the exposed service and requesting the public IP from `https://api.ipify.org` as the last step in the chain. For example, if the network is configured as per this use case, a `curl` request:
57
+
58
+ ```bash
59
+ curl -s http://external-cloud-run-service-a-ip/?url=http://internal-cloud-run-service-b-ip/?url=https://api.ipify.org?format=json
60
+ ```
61
+
62
+ will be received at Service A, then at Service B and finally at `https://api.ipify.org` will return the public IP.
63
+
64
+ ## Installation
65
+
66
+ The `urlprobe` tool is available on PiPy and can be installed via pip:
67
+
68
+ 1. **Install Python 3.10+:** Ensure you have Python 3.10 or a later version installed.
69
+ 2. **Install the package from PyPI using pip:**
70
+ ```bash
71
+ pip install urlprobe
72
+ ```
73
+ 3. **(optional) Install the package from TestPyPI using pip:**
74
+ ```bash
75
+ pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ urlprobe
76
+ ```
77
+
78
+ How you deploy the `urlprobe` tool depends on the type of infrastructure under test e.g. packaged as a `Docker` image or deployed as a cloud function.
79
+
80
+ ## Usage
81
+
82
+ ### Running as a standalone application
83
+
84
+ 1. **Run the server:**
85
+ ```bash
86
+ python -m urlprobe
87
+ ```
88
+
89
+ **(optionally) run the server with non-default args**
90
+ ```bash
91
+ python -m urlprobe --host 0.0.0.0 --port 8081 --debug
92
+ ```
93
+
94
+ 2. **Send a GET request to the server with the `url` parameter:**
95
+ ```bash
96
+ curl -s "http://<edge-service-ip>:8080/?url=https://<internal-service-ip>"
97
+ ```
98
+
99
+ Replace `<edge-service-ip>` with the IP address or hostname of your externally accessible edge service. For example, when running as a standalone application on localhost:
100
+
101
+ ```bash
102
+ curl -s "http://localhost:8080/?url=https://api.ipify.org?format=json"
103
+ ```
104
+
105
+ ## Output
106
+
107
+ The server will return a JSON object containing detailed information the request chain, including:
108
+
109
+ * **Edge Service Response:** Status code, headers, and body from the target service."
110
+ * **Error Messages:** Detailed error messages for any failures encountered during the request chain.
111
+
112
+ ## Contributing
113
+
114
+ Please see [Contributing Guidelines](https://github.com/av603/urlprobe/blob/main/CONTRIBUTING.md) for details on:
115
+
116
+ - Setting up your development environment
117
+ - Code style and standards
118
+ - Pull request process
119
+ - Running tests
120
+
121
+ # Versioning
122
+
123
+ This project follows [Semantic Versioning](https://semver.org/). For the versions available, see the [tags on this repository](https://github.com/av603/urlprobe/tags).
124
+
125
+ ## License
126
+
127
+ This project is licensed under the MIT License - see the [LICENSE](https://github.com/av603/urlprobe/blob/main/LICENSE) file for details.
128
+
@@ -0,0 +1,107 @@
1
+ # URL Probe
2
+
3
+ [![Test Status](https://github.com/av603/urlprobe/actions/workflows/test.yml/badge.svg)](https://github.com/av603/urlprobe/actions/workflows/test.yml)
4
+ [![Code Coverage](https://codecov.io/gh/av603/urlprobe/branch/main/graph/badge.svg)](https://codecov.io/gh/av603/urlprobe)
5
+
6
+ This tool helps test complex cloud network setups, like those with private services that have specific exit points. It's especially useful for checking if your cloud services can talk to each other within a virtual private cloud and those services can make requests to the outside world as expected.
7
+
8
+ **Use Case** Say you have a Cloud Load Balancer, two Cloud Run services (Service A and Service B) and a Cloud Subnetwork with a static egress IP. You have deployed this infrastructure and want to verify that the Cloud Load Balancer can access Service A, that Service A can access Service B and that requests from Service B appear from the subnetwork static egress IP.
9
+
10
+ ```
11
+ Internet
12
+ |
13
+ |
14
+ +-------+
15
+ | |
16
+ | Cloud |
17
+ | Run | (A) - Publicly Accessible
18
+ +-------+
19
+ | (Target URL Parameter)
20
+ |
21
+ v (Internal Network Traffic)
22
+ +-------+
23
+ | |
24
+ | Cloud |
25
+ | Run | (B) - Internal Service
26
+ +-------+
27
+ | (Outbound Request via Static IP)
28
+ |
29
+ v
30
+ +---------------+
31
+ | External |
32
+ | Target Server |
33
+ +---------------+
34
+ ```
35
+
36
+ The network connectivity and the public IP can be verified by deploying the `urlprobe` tool to Service A and Service B and sending a chained `curl` request to the exposed service and requesting the public IP from `https://api.ipify.org` as the last step in the chain. For example, if the network is configured as per this use case, a `curl` request:
37
+
38
+ ```bash
39
+ curl -s http://external-cloud-run-service-a-ip/?url=http://internal-cloud-run-service-b-ip/?url=https://api.ipify.org?format=json
40
+ ```
41
+
42
+ will be received at Service A, then at Service B and finally at `https://api.ipify.org` will return the public IP.
43
+
44
+ ## Installation
45
+
46
+ The `urlprobe` tool is available on PiPy and can be installed via pip:
47
+
48
+ 1. **Install Python 3.10+:** Ensure you have Python 3.10 or a later version installed.
49
+ 2. **Install the package from PyPI using pip:**
50
+ ```bash
51
+ pip install urlprobe
52
+ ```
53
+ 3. **(optional) Install the package from TestPyPI using pip:**
54
+ ```bash
55
+ pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ urlprobe
56
+ ```
57
+
58
+ How you deploy the `urlprobe` tool depends on the type of infrastructure under test e.g. packaged as a `Docker` image or deployed as a cloud function.
59
+
60
+ ## Usage
61
+
62
+ ### Running as a standalone application
63
+
64
+ 1. **Run the server:**
65
+ ```bash
66
+ python -m urlprobe
67
+ ```
68
+
69
+ **(optionally) run the server with non-default args**
70
+ ```bash
71
+ python -m urlprobe --host 0.0.0.0 --port 8081 --debug
72
+ ```
73
+
74
+ 2. **Send a GET request to the server with the `url` parameter:**
75
+ ```bash
76
+ curl -s "http://<edge-service-ip>:8080/?url=https://<internal-service-ip>"
77
+ ```
78
+
79
+ Replace `<edge-service-ip>` with the IP address or hostname of your externally accessible edge service. For example, when running as a standalone application on localhost:
80
+
81
+ ```bash
82
+ curl -s "http://localhost:8080/?url=https://api.ipify.org?format=json"
83
+ ```
84
+
85
+ ## Output
86
+
87
+ The server will return a JSON object containing detailed information the request chain, including:
88
+
89
+ * **Edge Service Response:** Status code, headers, and body from the target service."
90
+ * **Error Messages:** Detailed error messages for any failures encountered during the request chain.
91
+
92
+ ## Contributing
93
+
94
+ Please see [Contributing Guidelines](https://github.com/av603/urlprobe/blob/main/CONTRIBUTING.md) for details on:
95
+
96
+ - Setting up your development environment
97
+ - Code style and standards
98
+ - Pull request process
99
+ - Running tests
100
+
101
+ # Versioning
102
+
103
+ This project follows [Semantic Versioning](https://semver.org/). For the versions available, see the [tags on this repository](https://github.com/av603/urlprobe/tags).
104
+
105
+ ## License
106
+
107
+ This project is licensed under the MIT License - see the [LICENSE](https://github.com/av603/urlprobe/blob/main/LICENSE) file for details.
@@ -0,0 +1,61 @@
1
+ [tool.poetry]
2
+ name = "urlprobe"
3
+ version = "0.1.0"
4
+ description = "A tool to request a specified `url` and return the response."
5
+ authors = ["Andrew Vella"]
6
+ readme = "README.md"
7
+ packages = [{include = "urlprobe", from = "src"}]
8
+
9
+ [tool.poetry.dependencies]
10
+ python = "^3.10"
11
+ click = "^8.2.1"
12
+ rich = "^14.0.0"
13
+ flask = "^3.0.0"
14
+ toml = "^0.10.2"
15
+ requests = "^2.32.3"
16
+
17
+ [tool.poetry.group.dev.dependencies]
18
+ pytest = "^9.0.0"
19
+ pytest-cov = "^6.0.0"
20
+ flake8 = "^7.0.0"
21
+ pre-commit = "^4.0.0"
22
+ mypy = "^1.7.1"
23
+ types-requests = "^2.31.0"
24
+ types-toml = "^0.10.8"
25
+ python-semantic-release = "^8.0.0"
26
+
27
+ [tool.poetry.scripts]
28
+ urlprobe = "urlprobe.__main__:main"
29
+
30
+ [build-system]
31
+ requires = ["poetry-core"]
32
+ build-backend = "poetry.core.masonry.api"
33
+
34
+ [tool.semantic_release]
35
+ version_toml = ["pyproject.toml:tool.poetry.version"]
36
+ branch = "main"
37
+ upload_to_pypi = false
38
+ build_command = "poetry build"
39
+ upload_dists = true
40
+ tag_commit = true
41
+ tag_format = "{version}"
42
+ commit_parser = "angular"
43
+ changelog_file = "CHANGELOG.md"
44
+ changelog_header = """
45
+ # Changelog
46
+
47
+ All notable changes to this project will be documented in this file.
48
+
49
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
50
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
51
+ """
52
+
53
+ changelog_sections = [
54
+ { section = "Features", hidden = false, tags = ["feat"] },
55
+ { section = "Bug Fixes", hidden = false, tags = ["fix"] },
56
+ { section = "Documentation", hidden = false, tags = ["docs"] },
57
+ { section = "Refactoring", hidden = false, tags = ["refactor"] },
58
+ { section = "Dependencies", hidden = false, tags = ["chore", "build"] },
59
+ { section = "Performance", hidden = false, tags = ["perf"] },
60
+ { section = "Continuous Integration", hidden = true, tags = ["ci"] },
61
+ ]
@@ -0,0 +1,9 @@
1
+ """
2
+ A utility to probe url endpoints.
3
+
4
+ Requests a specified `url` and returns the response. Used for network
5
+ infrastructure probing and diagnostics.
6
+
7
+ For more detailed information, see the documentation at:
8
+ https://github.com/av603/urlprobe
9
+ """
@@ -0,0 +1,49 @@
1
+ """
2
+ Command-line entry point for the urlprobe Flask application.
3
+
4
+ This script parses command-line arguments for configuring the host,
5
+ port, and debug mode of the urlprobe web service and then starts
6
+ the Flask development server.
7
+ """
8
+
9
+ import argparse
10
+
11
+ from urlprobe.app import create_app, logger
12
+
13
+
14
+ def main():
15
+ """Entry point for the urlprobe Flask application."""
16
+ parser = argparse.ArgumentParser(
17
+ description="Run the urlprobe application."
18
+ )
19
+ parser.add_argument(
20
+ "--host",
21
+ type=str,
22
+ default="0.0.0.0",
23
+ help="The interface to bind to (default: 0.0.0.0)",
24
+ )
25
+ parser.add_argument(
26
+ "--port",
27
+ type=int,
28
+ default=8080,
29
+ help="The port to listen on (default: 8080)",
30
+ )
31
+ parser.add_argument(
32
+ "--debug",
33
+ action="store_true",
34
+ default=False,
35
+ help="Enable debug mode (default: False)",
36
+ )
37
+ args = parser.parse_args()
38
+
39
+ app = create_app()
40
+
41
+ logger.info(
42
+ f"Starting urlprobe on host {args.host}, port {args.port}, "
43
+ f"debug={args.debug}"
44
+ )
45
+ app.run(host=args.host, port=args.port, debug=args.debug)
46
+
47
+
48
+ if __name__ == "__main__":
49
+ main()
@@ -0,0 +1,142 @@
1
+ """Flask application module for URL Probe."""
2
+
3
+ import importlib.metadata
4
+ import logging
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Dict
8
+
9
+ import requests
10
+ import toml
11
+ from flask import Flask, jsonify, request
12
+
13
+ from urlprobe.utils import is_valid_url
14
+
15
+ # Configure logging at module level
16
+ logging.basicConfig(
17
+ level=logging.INFO,
18
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
19
+ datefmt="%Y-%m-%d %H:%M:%S",
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ DEFAULT_TIMEOUT = 10 # seconds
25
+ ALLOWED_METHODS = ["GET"]
26
+ VERIFY_SSL = True
27
+
28
+
29
+ @dataclass
30
+ class Probe:
31
+ """Probe data structure.
32
+
33
+ Attributes:
34
+ url: URL that was probed
35
+
36
+ status_code: HTTP status code from the response
37
+ elapsed_ms: Response time in milliseconds
38
+ final_url: Final URL after redirects
39
+ headers: Response headers
40
+ body: Response body
41
+ """
42
+
43
+ url: str
44
+
45
+ status_code: int = 0
46
+ elapsed_ms: int = 0
47
+ final_url: str = ""
48
+ headers: Dict[str, str] = field(default_factory=dict)
49
+ body: str = ""
50
+
51
+ def to_json(self):
52
+ """Convert Probe to json."""
53
+ return jsonify(self.__dict__)
54
+
55
+
56
+ def get_version() -> str:
57
+ """Read version from pyproject.toml."""
58
+ try:
59
+ pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
60
+ with open(pyproject_path, "r") as f:
61
+ pyproject = toml.load(f)
62
+ return pyproject["tool"]["poetry"]["version"]
63
+ except Exception as e:
64
+ logger.warning(f"Failed to read version from pyproject.toml: {e}")
65
+ return "unknown"
66
+
67
+
68
+ def handle_invalid_arg(logger, error_msg, code):
69
+ """Handle invalid args."""
70
+ logger.error(error_msg)
71
+ return {"error": error_msg}, code
72
+
73
+
74
+ def handle_request():
75
+ """Handle probe requests.
76
+
77
+ Returns:
78
+ Probe: Object containing response information
79
+ """
80
+ url = request.args.get("url")
81
+
82
+ if not is_valid_url(url):
83
+ return handle_invalid_arg(
84
+ logger, f"Missing or invalid `url` arg:{url}", 400
85
+ )
86
+
87
+ # send request to the URL and get the response
88
+ logger.info("Probing URL: %s", url)
89
+ resp = requests.request(
90
+ method=request.method,
91
+ url=url,
92
+ timeout=DEFAULT_TIMEOUT,
93
+ verify=VERIFY_SSL,
94
+ )
95
+
96
+ probe_response = Probe(
97
+ url=url,
98
+ status_code=resp.status_code,
99
+ body=resp.text,
100
+ headers=dict(resp.headers),
101
+ elapsed_ms=resp.elapsed.total_seconds(),
102
+ final_url=resp.url,
103
+ )
104
+
105
+ return probe_response.to_json()
106
+
107
+
108
+ def health_check():
109
+ """Check the health status and version of the service.
110
+
111
+ Returns:
112
+ dict: Health status and version information with format:
113
+ {"status": "healthy", "version": "x.y.z"}
114
+ """
115
+ try:
116
+ version = importlib.metadata.version("urlprobe")
117
+ logger.info("Health check requested, version: %s", version)
118
+ except importlib.metadata.PackageNotFoundError:
119
+ logger.warning(
120
+ "Package 'urlprobe' not found. Unable to determine version."
121
+ )
122
+ version = "unknown"
123
+
124
+ return {
125
+ "status": "healthy",
126
+ "version": version,
127
+ }
128
+
129
+
130
+ def create_app():
131
+ """Create and configure the Flask application.
132
+
133
+ Returns:
134
+ Flask: Configured Flask application instance
135
+ """
136
+ app = Flask(__name__)
137
+ logger.debug("Creating Flask application")
138
+
139
+ app.route("/", methods=ALLOWED_METHODS)(handle_request)
140
+ app.route("/health")(health_check)
141
+
142
+ return app
@@ -0,0 +1,30 @@
1
+ """Provide command line interface for URL Probe."""
2
+
3
+ import click
4
+
5
+ from urlprobe.app import create_app
6
+
7
+
8
+ @click.group()
9
+ def cli():
10
+ """Define URL Probe CLI group."""
11
+ pass
12
+
13
+
14
+ @cli.command()
15
+ @click.option("--host", default="127.0.0.1", help="Host to bind to")
16
+ @click.option("--port", default=8080, help="Port to bind to")
17
+ @click.option("--debug", is_flag=True, help="Enable debug mode")
18
+ def serve(host: str, port: int, debug: bool):
19
+ """Start the Flask server."""
20
+ app = create_app()
21
+ app.run(host=host, port=port, debug=debug)
22
+
23
+
24
+ def main():
25
+ """Execute the main CLI program."""
26
+ cli()
27
+
28
+
29
+ if __name__ == "__main__":
30
+ main()
@@ -0,0 +1,33 @@
1
+ """Utility functions for URL Probe."""
2
+
3
+ from urllib.parse import urlparse
4
+
5
+
6
+ def is_valid_url(url: str) -> bool:
7
+ """Check if a given string is a valid URL.
8
+
9
+ Args:
10
+ url: The URL string to validate
11
+
12
+ Returns:
13
+ bool: True if URL is valid, False otherwise
14
+
15
+ Examples:
16
+ >>> is_valid_url("https://example.com")
17
+ True
18
+ >>> is_valid_url("not_a_url")
19
+ False
20
+ """
21
+ if not url:
22
+ return False
23
+
24
+ try:
25
+ result = urlparse(url)
26
+ return all(
27
+ [
28
+ result.scheme in ("http", "https"),
29
+ result.netloc,
30
+ ]
31
+ )
32
+ except (AttributeError, ValueError):
33
+ return False