Skip to content

Commit

Permalink
Merge pull request #137 from piercefreeman/feature/add-network-signal
Browse files Browse the repository at this point in the history
Add optional fetch abort param
  • Loading branch information
piercefreeman authored Nov 20, 2024
2 parents dced425 + 16972e1 commit 16e1e97
Show file tree
Hide file tree
Showing 9 changed files with 330 additions and 117 deletions.
181 changes: 164 additions & 17 deletions .github/scripts/check_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
"""

from dataclasses import dataclass
from logging import info
from sys import stdout
from typing import Optional, Union

import toml
from packaging import specifiers, version

IGNORE_PACKAGE_NAMES = {"python"}

Expand All @@ -17,45 +20,189 @@
class Package:
name: str
extras: frozenset
version: Optional[str] = None


def normalize_version_constraint(version_str: Optional[str]) -> Optional[str]:
"""Convert different version constraints to a normalized form."""
if not version_str:
return None

# Handle caret notation
if version_str.startswith("^"):
ver = version_str[1:] # Remove caret
base_version = version.parse(ver)
return f">={ver},<{base_version.major + 1}.0.0"

# Handle tilde notation
if version_str.startswith("~"):
ver = version_str[1:] # Remove tilde
base_version = version.parse(ver)
return f">={ver},<{base_version.major}.{base_version.minor + 1}.0"

return version_str


def parse_version_spec(value: Union[str, dict]) -> Optional[str]:
"""Parse version specification from poetry dependency definition."""
if isinstance(value, str):
return value
elif isinstance(value, dict) and "version" in value:
return value["version"] # type: ignore
return None


def parse_poetry_dependencies(data) -> set[Package]:
deps = set()
for key, value in data.items():
if key in IGNORE_PACKAGE_NAMES:
continue
package_name = key.split("[")[0]

# Handle package name with optional extras
package_parts = key.split("[")
package_name = package_parts[0]

# Parse extras and version
extras: frozenset[str] = frozenset()
if isinstance(value, dict) and "version" in value and "extras" in value:
extras = frozenset(value["extras"])
deps.add(Package(name=package_name, extras=extras))
version: Optional[str] = None

if isinstance(value, dict):
if "extras" in value:
extras = frozenset(value["extras"])
version = parse_version_spec(value)
else:
version = parse_version_spec(value)

# Normalize version constraint
normalized_version = normalize_version_constraint(version)

pkg = Package(name=package_name, extras=extras, version=normalized_version)
info(f"Poetry dependency: {pkg}")
deps.add(pkg)
return deps


def parse_project_dependencies(data) -> set[Package]:
deps = set()
for dep in data:
parts = dep.split("[")
package_name = parts[0]
if package_name in IGNORE_PACKAGE_NAMES:
continue
extras = frozenset(parts[1][:-1].split(",")) if len(parts) > 1 else frozenset()
deps.add(Package(name=package_name, extras=extras))
info(f"\nParsing project dependency: {dep}")

if "[" in dep:
# Split name and extras+version
base_name, rest = dep.split("[", 1)
base_name = base_name.strip()

# Find closing bracket for extras
bracket_idx = rest.find("]")
if bracket_idx == -1:
continue

# Split extras and version info
extras_str = rest[:bracket_idx]
version_str = rest[bracket_idx + 1 :].strip()

# Parse extras
extras = frozenset(part.strip() for part in extras_str.split(","))

# Create package with extras and version
pkg = Package(
name=base_name,
extras=extras,
version=normalize_version_constraint(
version_str if version_str else None
),
)
info(f" Created package: {pkg}")
deps.add(pkg)

else:
# Handle version specs in the package name
if any(op in dep for op in [">=", "<=", "==", "<", ">"]):
for op in [">=", "<=", "==", "<", ">"]:
if op in dep:
name, version = dep.split(op, 1)
pkg = Package(
name=name.strip(),
extras=frozenset(),
version=normalize_version_constraint(
f"{op}{version.strip()}"
),
)
info(f" Created package with version: {pkg}")
deps.add(pkg)
break
else:
pkg = Package(name=dep.strip(), extras=frozenset(), version=None)
info(f" Created simple package: {pkg}")
deps.add(pkg)
return deps


def compare_dependencies(poetry_deps: set[Package], project_deps: set[Package]):
missing_in_project = poetry_deps - project_deps
missing_in_poetry = project_deps - poetry_deps

if missing_in_project or missing_in_poetry:
info("\nPoetry dependencies:")
for dep in poetry_deps:
info(f" {dep}")

info("\nProject dependencies:")
for dep in project_deps:
info(f" {dep}")

# First compare just names and extras
poetry_base = {Package(name=p.name, extras=p.extras) for p in poetry_deps}
project_base = {Package(name=p.name, extras=p.extras) for p in project_deps}

missing_in_project = poetry_base - project_base
missing_in_poetry = project_base - poetry_base

# Then check for version mismatches in matching packages
version_mismatches = []
for poetry_pkg in poetry_deps:
for project_pkg in project_deps:
if (
poetry_pkg.name == project_pkg.name
and poetry_pkg.extras == project_pkg.extras
):
if not are_version_constraints_compatible(
poetry_pkg.version, project_pkg.version
):
version_mismatches.append((poetry_pkg, project_pkg))

if missing_in_project or missing_in_poetry or version_mismatches:
if missing_in_project:
stdout.write(f"Missing in project dependencies: {missing_in_project}")
stdout.write(f"Missing in project dependencies: {missing_in_project}\n")
if missing_in_poetry:
stdout.write(f"Missing in Poetry dependencies: {missing_in_poetry}")
stdout.write(f"Missing in Poetry dependencies: {missing_in_poetry}\n")
if version_mismatches:
stdout.write("Version mismatches found:\n")
for poetry_pkg, project_pkg in version_mismatches:
stdout.write(
f" {poetry_pkg.name}: Poetry={poetry_pkg.version}, Project={project_pkg.version}\n"
)
exit(1)
else:
stdout.write("All dependencies match!")
stdout.write("All dependencies match!\n")


def are_version_constraints_compatible(
ver1: Optional[str], ver2: Optional[str]
) -> bool:
"""Check if two version constraints are semantically equivalent."""
if ver1 == ver2:
return True
if ver1 is None or ver2 is None:
return False

try:
# Parse both version constraints
spec1 = specifiers.SpecifierSet(ver1)
spec2 = specifiers.SpecifierSet(ver2)

# Check if they match the same versions
test_versions = ["2.0.0", "2.5.0", "2.5.3", "2.9.9", "3.0.0", "3.0.1", "4.0.0"]

return all((str(v) in spec1) == (str(v) in spec2) for v in test_versions)
except Exception:
return False


if __name__ == "__main__":
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install toml
pip install toml packaging
- name: Check venv dependencies match Poetry dependencies
run: python .github/scripts/check_dependencies.py
47 changes: 39 additions & 8 deletions mountaineer/__tests__/client_builder/test_build_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,21 @@ def fn2():
(
"""
export const my_method_fn = (
{requestBody}: {requestBody: ExampleModel}
{
requestBody,
signal
}: {
requestBody: ExampleModel,
signal?: AbortSignal
} = {}
): Promise<ExampleResponseModel> => {
return __request({
'method': 'POST',
'url': '/testing/url',
'errors': {
422: HTTPValidationErrorException
},
signal,
'body': requestBody,
'mediaType': 'application/json'
});
Expand Down Expand Up @@ -143,13 +150,20 @@ def fn2():
),
(
"""
export const my_method_fn = (): Promise<ExampleResponseModel> => {
export const my_method_fn = (
{
signal
} : {
signal?: AbortSignal
} = {}
): Promise<ExampleResponseModel> => {
return __request({
'method': 'POST',
'url': '/testing/url',
'errors': {
422: HTTPValidationErrorException
}
},
signal
});
}
"""
Expand Down Expand Up @@ -229,13 +243,15 @@ def fn2():
item_id,
query_param_required_id,
query_param_optional_id,
requestBody
requestBody,
signal
}: {
item_id: string,
query_param_required_id: string,
query_param_optional_id?: string,
requestBody: ExampleModel
}
requestBody: ExampleModel,
signal?: AbortSignal
} = {}
): Promise<ExampleResponseModel> => {
return __request({
'method': 'POST',
Expand All @@ -247,6 +263,7 @@ def fn2():
query_param_required_id,
query_param_optional_id
},
signal,
'body': requestBody,
'mediaType': 'application/json'
});
Expand Down Expand Up @@ -301,14 +318,21 @@ def test_build_action(
(
"""
export const my_method_fn = (
{requestBody}: {requestBody: ExampleModel}
{
requestBody,
signal
}: {
requestBody: ExampleModel,
signal?: AbortSignal
} = {}
): Promise<AsyncGenerator<ExampleResponseModel, void, unknown>> => {
return __request({
'method': 'POST',
'url': '/testing/url',
'errors': {
422: HTTPValidationErrorException
},
signal,
'body': requestBody,
'mediaType': 'application/json',
'eventStreamResponse': true
Expand Down Expand Up @@ -364,14 +388,21 @@ def test_build_server_side_event_action(
(
"""
export const my_method_fn = (
{requestBody}: {requestBody: ExampleModel}
{
requestBody,
signal
}: {
requestBody: ExampleModel,
signal?: AbortSignal
} = {}
): Promise<Response> => {
return __request({
'method': 'POST',
'url': '/testing/url',
'errors': {
422: HTTPValidationErrorException
},
signal,
'body': requestBody,
'mediaType': 'application/json',
'outputFormat': 'raw'
Expand Down
2 changes: 2 additions & 0 deletions mountaineer/__tests__/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def test_exception_action(self) -> None:
openapi_spec = app.generate_openapi()
openapi_definition = OpenAPIDefinition(**openapi_spec)

assert openapi_definition.components.schemas
assert openapi_definition.components.schemas.keys() == {
"TestExceptionActionResponse",
"ExampleException",
Expand Down Expand Up @@ -157,6 +158,7 @@ def test_exception_action(self) -> None:
openapi_spec = app.generate_openapi()
openapi_definition = OpenAPIDefinition(**openapi_spec)

assert openapi_definition.components.schemas
assert openapi_definition.components.schemas.keys() == {
"TestExceptionActionResponse",
"mountaineer.__tests__.test_1.ExampleException",
Expand Down
Loading

0 comments on commit 16e1e97

Please sign in to comment.