Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit e023af3

Browse files
ix-56hfilipchristiansen
andauthored
replace curl by httpx (#374)
* replace curl by httpx --------- Co-authored-by: Filip Christiansen <22807962+filipchristiansen@users.noreply.github.com>
1 parent bc8cdb4 commit e023af3

File tree

5 files changed

+32
-87
lines changed

5 files changed

+32
-87
lines changed

‎pyproject.toml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ requires-python = ">= 3.8"
77
dependencies = [
88
"click>=8.0.0",
99
"fastapi[standard]>=0.109.1", # Minimum safe release (https://osv.dev/vulnerability/PYSEC-2024-38)
10+
"httpx",
11+
"pathspec>=0.12.1",
1012
"pydantic",
1113
"python-dotenv",
1214
"slowapi",
1315
"starlette>=0.40.0", # Minimum safe release (https://osv.dev/vulnerability/GHSA-f96h-pmfr-66vw)
1416
"tiktoken>=0.7.0", # Support for o200k_base encoding
15-
"pathspec>=0.12.1",
1617
"typing_extensions>= 4.0.0; python_version < '3.10'",
1718
"uvicorn>=0.11.7", # Minimum safe release (https://osv.dev/vulnerability/PYSEC-2020-150)
1819
]

‎requirements.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
click>=8.0.0
22
fastapi[standard]>=0.109.1 # Vulnerable to https://osv.dev/vulnerability/PYSEC-2024-38
3+
httpx
34
pathspec>=0.12.1
45
pydantic
56
python-dotenv

‎src/gitingest/utils/git_utils.py‎

Lines changed: 13 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,12 @@
44

55
import asyncio
66
import base64
7-
import os
87
import re
98
from typing import Final
109
from urllib.parse import urlparse
1110

12-
from starlette.status import (
13-
HTTP_200_OK,
14-
HTTP_301_MOVED_PERMANENTLY,
15-
HTTP_302_FOUND,
16-
HTTP_401_UNAUTHORIZED,
17-
HTTP_403_FORBIDDEN,
18-
HTTP_404_NOT_FOUND,
19-
)
11+
import httpx
12+
from starlette.status import HTTP_200_OK, HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND
2013

2114
from gitingest.utils.compat_func import removesuffix
2215
from gitingest.utils.exceptions import InvalidGitHubTokenError
@@ -115,45 +108,28 @@ async def check_repo_exists(url: str, token: str | None = None) -> bool:
115108
If the host returns an unrecognised status code.
116109
117110
"""
118-
# TODO: use `requests` instead of `curl`
119-
cmd: list[str] = [
120-
"curl",
121-
"--silent", # Suppress output
122-
"--location", # Follow redirects
123-
"--write-out",
124-
"%{http_code}", # Write the HTTP status code to stdout
125-
"-o",
126-
os.devnull,
127-
]
111+
headers = {}
128112

129113
if token and is_github_host(url):
130114
host, owner, repo = _parse_github_url(url)
131115
# Public GitHub vs. GitHub Enterprise
132116
base_api = "https://api.github.com" if host == "github.com" else f"https://{host}/api/v3"
133117
url = f"{base_api}/repos/{owner}/{repo}"
134-
cmd+= ["--header", f"Authorization: Bearer {token}"]
118+
headers["Authorization"] =f"Bearer {token}"
135119

136-
cmd.append(url)
120+
async with httpx.AsyncClient(follow_redirects=True) as client:
121+
try:
122+
response = await client.head(url, headers=headers)
123+
except httpx.RequestError:
124+
return False
137125

138-
proc = await asyncio.create_subprocess_exec(
139-
*cmd,
140-
stdout=asyncio.subprocess.PIPE,
141-
stderr=asyncio.subprocess.PIPE,
142-
)
143-
stdout, _ = await proc.communicate()
126+
status_code = response.status_code
144127

145-
if proc.returncode != 0:
146-
return False
147-
148-
status = int(stdout.decode().strip())
149-
if status in {HTTP_200_OK, HTTP_301_MOVED_PERMANENTLY}:
128+
if status_code == HTTP_200_OK:
150129
return True
151-
# TODO: handle 302 redirects
152-
if status in {HTTP_404_NOT_FOUND, HTTP_302_FOUND}:
153-
return False
154-
if status in {HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN}:
130+
if status_code in {HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND}:
155131
return False
156-
msg = f"Unexpected HTTP status {status} for {url}"
132+
msg = f"Unexpected HTTP status {status_code} for {url}"
157133
raise RuntimeError(msg)
158134

159135

‎tests/test_clone.py‎

Lines changed: 15 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
from pathlib import Path
1010
from unittest.mock import AsyncMock
1111

12+
import httpx
1213
import pytest
1314
from pytest_mock import MockerFixture
15+
from starlette.status import HTTP_200_OK, HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND
1416

1517
from gitingest.clone import clone_repo
1618
from gitingest.schemas import CloneConfig
@@ -87,35 +89,24 @@ async def test_clone_nonexistent_repository(repo_exists_true: AsyncMock) -> None
8789

8890
@pytest.mark.asyncio
8991
@pytest.mark.parametrize(
90-
("mock_stdout", "return_code", "expected"),
92+
("status_code", "expected"),
9193
[
92-
(b"200\n", 0, True), # Existing repo
93-
(b"404\n", 0, False), # Non-existing repo
94-
(b"200\n", 1, False), # Failed request
94+
(HTTP_200_OK, True),
95+
(HTTP_401_UNAUTHORIZED, False),
96+
(HTTP_403_FORBIDDEN, False),
97+
(HTTP_404_NOT_FOUND, False),
9598
],
9699
)
97-
async def test_check_repo_exists(
98-
mock_stdout: bytes,
99-
*,
100-
return_code: int,
101-
expected: bool,
102-
mocker: MockerFixture,
103-
) -> None:
104-
"""Test the ``check_repo_exists`` function with different Git HTTP responses.
105-
106-
Given various stdout lines and return codes:
107-
When ``check_repo_exists`` is called,
108-
Then it should correctly indicate whether the repository exists.
109-
"""
110-
mock_exec = mocker.patch("asyncio.create_subprocess_exec", new_callable=AsyncMock)
111-
mock_process = AsyncMock()
112-
mock_process.communicate.return_value = (mock_stdout, b"")
113-
mock_process.returncode = return_code
114-
mock_exec.return_value = mock_process
100+
async def test_check_repo_exists(status_code: int, *, expected: bool, mocker: MockerFixture) -> None:
101+
"""Verify that ``check_repo_exists`` interprets httpx results correctly."""
102+
mock_client = AsyncMock()
103+
mock_client.__aenter__.return_value = mock_client # context-manager protocol
104+
mock_client.head.return_value = httpx.Response(status_code=status_code)
105+
mocker.patch("httpx.AsyncClient", return_value=mock_client)
115106

116-
repo_exists = await check_repo_exists(DEMO_URL)
107+
result = await check_repo_exists(DEMO_URL)
117108

118-
assert repo_exists is expected
109+
assert result is expected
119110

120111

121112
@pytest.mark.asyncio
@@ -218,25 +209,6 @@ async def test_check_repo_exists_with_redirect(mocker: MockerFixture) -> None:
218209
assert repo_exists is False
219210

220211

221-
@pytest.mark.asyncio
222-
async def test_check_repo_exists_with_permanent_redirect(mocker: MockerFixture) -> None:
223-
"""Test ``check_repo_exists`` when a permanent redirect (301) is returned.
224-
225-
Given a URL that responds with "301 Found":
226-
When ``check_repo_exists`` is called,
227-
Then it should return ``True``, indicating the repo may exist at the new location.
228-
"""
229-
mock_exec = mocker.patch("asyncio.create_subprocess_exec", new_callable=AsyncMock)
230-
mock_process = AsyncMock()
231-
mock_process.communicate.return_value = (b"301\n", b"")
232-
mock_process.returncode = 0 # Simulate successful request
233-
mock_exec.return_value = mock_process
234-
235-
repo_exists = await check_repo_exists(DEMO_URL)
236-
237-
assert repo_exists
238-
239-
240212
@pytest.mark.asyncio
241213
async def test_clone_with_timeout(run_command_mock: AsyncMock) -> None:
242214
"""Test cloning a repository when a timeout occurs.

‎tests/test_git_utils.py‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,7 @@
1212
import pytest
1313

1414
from gitingest.utils.exceptions import InvalidGitHubTokenError
15-
from gitingest.utils.git_utils import (
16-
create_git_auth_header,
17-
create_git_command,
18-
is_github_host,
19-
validate_github_token,
20-
)
15+
from gitingest.utils.git_utils import create_git_auth_header, create_git_command, is_github_host, validate_github_token
2116

2217
if TYPE_CHECKING:
2318
from pathlib import Path

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /