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 +21 -0
- urlprobe-0.1.0/PKG-INFO +128 -0
- urlprobe-0.1.0/README.md +107 -0
- urlprobe-0.1.0/pyproject.toml +61 -0
- urlprobe-0.1.0/src/urlprobe/__init__.py +9 -0
- urlprobe-0.1.0/src/urlprobe/__main__.py +49 -0
- urlprobe-0.1.0/src/urlprobe/app.py +142 -0
- urlprobe-0.1.0/src/urlprobe/cli.py +30 -0
- urlprobe-0.1.0/src/urlprobe/utils.py +33 -0
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.
|
urlprobe-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/av603/urlprobe/actions/workflows/test.yml)
|
|
24
|
+
[](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
|
+
|
urlprobe-0.1.0/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# URL Probe
|
|
2
|
+
|
|
3
|
+
[](https://github.com/av603/urlprobe/actions/workflows/test.yml)
|
|
4
|
+
[](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,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
|