Skip to content

Commit 75a014c

Browse files
authored
Merge pull request #224 from posit-dev/feat-pull-data-from-github
feat: enable data to be obtained via GitHub URLs
2 parents 3a6f2a3 + f7459c7 commit 75a014c

3 files changed

Lines changed: 278 additions & 35 deletions

File tree

pointblank/datascan.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ class DataScan:
5757
----------
5858
data
5959
The data to scan and summarize. This could be a DataFrame object, an Ibis table object,
60-
a CSV file path, a Parquet file path, or a database connection string.
60+
a CSV file path, a Parquet file path, a GitHub URL pointing to a CSV or Parquet file,
61+
or a database connection string.
6162
tbl_name
6263
Optionally, the name of the table could be provided as `tbl_name`.
6364
@@ -127,10 +128,14 @@ def __init__(self, data: IntoFrameT, tbl_name: str | None = None) -> None:
127128
from pointblank.validate import (
128129
_process_connection_string,
129130
_process_csv_input,
131+
_process_github_url,
130132
_process_parquet_input,
131133
)
132134

133135
# Process input data to handle different data source types
136+
# Handle GitHub URL input (e.g., "https://github.com/user/repo/blob/main/data.csv")
137+
data = _process_github_url(data)
138+
134139
# Handle connection string input (e.g., "duckdb:///path/to/file.ddb::table_name")
135140
data = _process_connection_string(data)
136141

@@ -557,6 +562,7 @@ def col_summary_tbl(data: FrameT | Any, tbl_name: str | None = None) -> GT:
557562
- CSV files (string path or `pathlib.Path` object with `.csv` extension)
558563
- Parquet files (string path, `pathlib.Path` object, glob pattern, directory with `.parquet`
559564
extension, or partitioned dataset)
565+
- GitHub URLs (direct links to CSV or Parquet files on GitHub)
560566
- Database connection strings (URI format with optional table specification)
561567
562568
The table types marked with an asterisk need to be prepared as Ibis tables (with type of
@@ -593,10 +599,14 @@ def col_summary_tbl(data: FrameT | Any, tbl_name: str | None = None) -> GT:
593599
from pointblank.validate import (
594600
_process_connection_string,
595601
_process_csv_input,
602+
_process_github_url,
596603
_process_parquet_input,
597604
)
598605

599606
# Process input data to handle different data source types
607+
# Handle GitHub URL input (e.g., "https://github.com/user/repo/blob/main/data.csv")
608+
data = _process_github_url(data)
609+
600610
# Handle connection string input (e.g., "duckdb:///path/to/file.ddb::table_name")
601611
data = _process_connection_string(data)
602612

pointblank/validate.py

Lines changed: 151 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,112 @@ def get_data_path(
740740
# =============================================================================
741741

742742

743+
def _process_github_url(data: FrameT | Any) -> FrameT | Any:
744+
"""
745+
Process data parameter to handle GitHub URLs pointing to CSV or Parquet files.
746+
747+
Handles both standard GitHub URLs and raw GitHub content URLs, downloading the content
748+
and processing it as a local file.
749+
750+
Supports:
751+
- Standard github.com URLs pointing to CSV or Parquet files (automatically transformed to raw URLs)
752+
- Raw raw.githubusercontent.com URLs pointing to CSV or Parquet files (processed directly)
753+
- Both CSV and Parquet file formats
754+
- Automatic temporary file management and cleanup
755+
756+
Parameters
757+
----------
758+
data : FrameT | Any
759+
The data parameter which may be a GitHub URL string or any other data type.
760+
761+
Returns
762+
-------
763+
FrameT | Any
764+
If the input is a supported GitHub URL, returns a DataFrame loaded from the downloaded file.
765+
Otherwise, returns the original data unchanged.
766+
767+
Examples
768+
--------
769+
Standard GitHub URL (automatically transformed):
770+
>>> url = "https://github.com/user/repo/blob/main/data.csv"
771+
>>> df = _process_github_url(url)
772+
773+
Raw GitHub URL (used directly):
774+
>>> raw_url = "https://raw.githubusercontent.com/user/repo/main/data.csv"
775+
>>> df = _process_github_url(raw_url)
776+
"""
777+
import re
778+
import tempfile
779+
from urllib.parse import urlparse
780+
from urllib.request import urlopen
781+
782+
# Check if data is a string that looks like a GitHub URL
783+
if not isinstance(data, str):
784+
return data
785+
786+
# Parse the URL to check if it's a GitHub URL
787+
try:
788+
parsed = urlparse(data)
789+
except Exception:
790+
return data
791+
792+
# Check if it's a GitHub URL (standard or raw)
793+
is_standard_github = parsed.netloc in ["github.com", "www.github.com"]
794+
is_raw_github = parsed.netloc == "raw.githubusercontent.com"
795+
796+
if not (is_standard_github or is_raw_github):
797+
return data
798+
799+
# Check if it points to a CSV or Parquet file
800+
path_lower = parsed.path.lower()
801+
if not (path_lower.endswith(".csv") or path_lower.endswith(".parquet")):
802+
return data
803+
804+
# Determine the raw URL to download from
805+
if is_raw_github:
806+
# Already a raw GitHub URL, use it directly
807+
raw_url = data
808+
else:
809+
# Transform GitHub URL to raw content URL
810+
# Pattern: https://github.com/user/repo/blob/branch/path/file.ext
811+
# Becomes: https://raw.githubusercontent.com/user/repo/branch/path/file.ext
812+
github_pattern = r"github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.+)"
813+
match = re.search(github_pattern, data)
814+
815+
if not match:
816+
# If URL doesn't match expected GitHub blob pattern, return original data
817+
return data
818+
819+
user, repo, branch, file_path = match.groups()
820+
raw_url = f"https://raw.githubusercontent.com/{user}/{repo}/{branch}/{file_path}"
821+
822+
# Download the file content to a temporary file
823+
try:
824+
with urlopen(raw_url) as response:
825+
content = response.read()
826+
827+
# Determine file extension
828+
file_ext = ".csv" if path_lower.endswith(".csv") else ".parquet"
829+
830+
# Create a temporary file
831+
with tempfile.NamedTemporaryFile(mode="wb", suffix=file_ext, delete=False) as tmp_file:
832+
tmp_file.write(content)
833+
tmp_file_path = tmp_file.name
834+
835+
# Process the temporary file using existing CSV or Parquet processing functions
836+
if file_ext == ".csv":
837+
return _process_csv_input(tmp_file_path)
838+
else: # .parquet
839+
return _process_parquet_input(tmp_file_path)
840+
841+
except Exception:
842+
# If download or processing fails, return original data
843+
return data
844+
845+
except Exception as e:
846+
raise RuntimeError(f"Failed to download or process GitHub file from {raw_url}: {e}") from e
847+
848+
743849
def _process_connection_string(data: FrameT | Any) -> FrameT | Any:
744850
"""
745851
Process data parameter to handle database connection strings.
@@ -1215,6 +1321,9 @@ def preview(
12151321
"""
12161322

12171323
# Process input data to handle different data source types
1324+
# Handle GitHub URL input (e.g., "https://github.com/user/repo/blob/main/data.csv")
1325+
data = _process_github_url(data)
1326+
12181327
# Handle connection string input (e.g., "duckdb:///path/to/file.ddb::table_name")
12191328
data = _process_connection_string(data)
12201329

@@ -1707,6 +1816,9 @@ def missing_vals_tbl(data: FrameT | Any) -> GT:
17071816
"""
17081817

17091818
# Process input data to handle different data source types
1819+
# Handle GitHub URL input (e.g., "https://github.com/user/repo/blob/main/data.csv")
1820+
data = _process_github_url(data)
1821+
17101822
# Handle connection string input (e.g., "duckdb:///path/to/file.ddb::table_name")
17111823
data = _process_connection_string(data)
17121824

@@ -2223,6 +2335,11 @@ def get_column_count(data: FrameT | Any) -> int:
22232335
provided. The file will be automatically detected and loaded using the best available DataFrame
22242336
library. The loading preference is Polars first, then Pandas as a fallback.
22252337

2338+
GitHub URLs pointing to CSV or Parquet files are automatically detected and converted to raw
2339+
content URLs for downloading. The URL format should be:
2340+
`https://github.com/user/repo/blob/branch/path/file.csv` or
2341+
`https://github.com/user/repo/blob/branch/path/file.parquet`
2342+
22262343
Connection strings follow database URL formats and must also specify a table using the
22272344
`::table_name` suffix. Examples include:
22282345

@@ -2314,17 +2431,14 @@ def get_column_count(data: FrameT | Any) -> int:
23142431

23152432
# Process different input types
23162433
if isinstance(data, str) or isinstance(data, Path):
2317-
if "::" in str(data):
2318-
data = _process_connection_string(data)
2319-
elif str(data).endswith(".csv") or str(data).endswith(".CSV"):
2320-
data = _process_csv_input(data)
2321-
elif (
2322-
str(data).endswith(".parquet")
2323-
or str(data).endswith(".PARQUET")
2324-
or "*" in str(data)
2325-
or Path(data).is_dir()
2326-
):
2327-
data = _process_parquet_input(data)
2434+
# Process GitHub URLs first
2435+
data = _process_github_url(data)
2436+
# Handle connection string input
2437+
data = _process_connection_string(data)
2438+
# Handle CSV file input
2439+
data = _process_csv_input(data)
2440+
# Handle Parquet file input
2441+
data = _process_parquet_input(data)
23282442
elif isinstance(data, list):
23292443
# Handle list of file paths (likely Parquet files)
23302444
data = _process_parquet_input(data)
@@ -2385,6 +2499,7 @@ def get_row_count(data: FrameT | Any) -> int:
23852499
- CSV files (string path or `pathlib.Path` object with `.csv` extension)
23862500
- Parquet files (string path, `pathlib.Path` object, glob pattern, directory with `.parquet`
23872501
extension, or partitioned dataset)
2502+
- GitHub URLs (direct links to CSV or Parquet files on GitHub)
23882503
- Database connection strings (URI format with optional table specification)
23892504

23902505
The table types marked with an asterisk need to be prepared as Ibis tables (with type of
@@ -2396,6 +2511,11 @@ def get_row_count(data: FrameT | Any) -> int:
23962511
provided. The file will be automatically detected and loaded using the best available DataFrame
23972512
library. The loading preference is Polars first, then Pandas as a fallback.
23982513

2514+
GitHub URLs pointing to CSV or Parquet files are automatically detected and converted to raw
2515+
content URLs for downloading. The URL format should be:
2516+
`https://github.com/user/repo/blob/branch/path/file.csv` or
2517+
`https://github.com/user/repo/blob/branch/path/file.parquet`
2518+
23992519
Connection strings follow database URL formats and must also specify a table using the
24002520
`::table_name` suffix. Examples include:
24012521

@@ -2487,17 +2607,14 @@ def get_row_count(data: FrameT | Any) -> int:
24872607

24882608
# Process different input types
24892609
if isinstance(data, str) or isinstance(data, Path):
2490-
if "::" in str(data):
2491-
data = _process_connection_string(data)
2492-
elif str(data).endswith(".csv") or str(data).endswith(".CSV"):
2493-
data = _process_csv_input(data)
2494-
elif (
2495-
str(data).endswith(".parquet")
2496-
or str(data).endswith(".PARQUET")
2497-
or "*" in str(data)
2498-
or Path(data).is_dir()
2499-
):
2500-
data = _process_parquet_input(data)
2610+
# Process GitHub URLs first
2611+
data = _process_github_url(data)
2612+
# Handle connection string input
2613+
data = _process_connection_string(data)
2614+
# Handle CSV file input
2615+
data = _process_csv_input(data)
2616+
# Handle Parquet file input
2617+
data = _process_parquet_input(data)
25012618
elif isinstance(data, list):
25022619
# Handle list of file paths (likely Parquet files)
25032620
data = _process_parquet_input(data)
@@ -2905,13 +3022,15 @@ class Validate:
29053022
----------
29063023
data
29073024
The table to validate, which could be a DataFrame object, an Ibis table object, a CSV
2908-
file path, a Parquet file path, or a database connection string. When providing a CSV or
2909-
Parquet file path (as a string or `pathlib.Path` object), the file will be automatically
2910-
loaded using an available DataFrame library (Polars or Pandas). Parquet input also supports
2911-
glob patterns, directories containing .parquet files, and Spark-style partitioned datasets.
2912-
Connection strings enable direct database access via Ibis with optional table specification
2913-
using the `::table_name` suffix. Read the *Supported Input Table Types* section for details
2914-
on the supported table types.
3025+
file path, a Parquet file path, a GitHub URL pointing to a CSV or Parquet file, or a
3026+
database connection string. When providing a CSV or Parquet file path (as a string or
3027+
`pathlib.Path` object), the file will be automatically loaded using an available DataFrame
3028+
library (Polars or Pandas). Parquet input also supports glob patterns, directories
3029+
containing .parquet files, and Spark-style partitioned datasets. GitHub URLs are
3030+
automatically transformed to raw content URLs and downloaded. Connection strings enable
3031+
direct database access via Ibis with optional table specification using the `::table_name`
3032+
suffix. Read the *Supported Input Table Types* section for details on the supported table
3033+
types.
29153034
tbl_name
29163035
An optional name to assign to the input table object. If no value is provided, a name will
29173036
be generated based on whatever information is available. This table name will be displayed
@@ -3431,6 +3550,9 @@ def send_report():
34313550
locale: str | None = None
34323551

34333552
def __post_init__(self):
3553+
# Handle GitHub URL input for the data parameter
3554+
self.data = _process_github_url(self.data)
3555+
34343556
# Handle connection string input for the data parameter
34353557
self.data = _process_connection_string(self.data)
34363558

0 commit comments

Comments
 (0)