diff --git a/posting-1.13.0-py3-none-any.whl b/posting-1.13.0-py3-none-any.whl new file mode 100644 index 00000000..8b56eb1a Binary files /dev/null and b/posting-1.13.0-py3-none-any.whl differ diff --git a/src/posting/__main__.py b/src/posting/__main__.py index 0735ed85..e562f23c 100644 --- a/src/posting/__main__.py +++ b/src/posting/__main__.py @@ -116,11 +116,7 @@ def import_spec(spec_path: str, output: str | None, type: str) -> None: collection = import_openapi_spec(spec_path) elif type.lower() == "postman": spec_type = "Postman" - console.print( - "Importing Postman collection haven't been implemented yet", style="red" - ) - import_postman_spec(spec_path, output) - return + collection = import_postman_spec(spec_path, output) else: console.print(f"Unknown spec type: {type!r}", style="red") return diff --git a/src/posting/importing/postman.py b/src/posting/importing/postman.py index ebb7b256..220fcd01 100644 --- a/src/posting/importing/postman.py +++ b/src/posting/importing/postman.py @@ -1,15 +1,22 @@ from pathlib import Path -from typing import Any, List, Optional +from typing import List, Optional import json -import os import re -import yaml from pydantic import BaseModel from rich.console import Console -from posting.collection import APIInfo, Collection +from posting.collection import ( + APIInfo, + Collection, + FormItem, + Header, + QueryParam, + RequestBody, + RequestModel, + HttpRequestMethod, +) class Variable(BaseModel): @@ -22,9 +29,17 @@ class Variable(BaseModel): disabled: Optional[bool] = None +class RawRequestOptions(BaseModel): + language: str + + +class RequestOptions(BaseModel): + raw: RawRequestOptions + + class Body(BaseModel): mode: str - options: Optional[dict] = None + options: Optional[RequestOptions] = None raw: Optional[str] = None formdata: Optional[List[Variable]] = None @@ -37,7 +52,7 @@ class Url(BaseModel): class PostmanRequest(BaseModel): - method: str + method: HttpRequestMethod url: Optional[str | Url] = None header: Optional[List[Variable]] = None description: Optional[str] = None @@ -52,100 +67,111 @@ class RequestItem(BaseModel): class PostmanCollection(BaseModel): info: dict[str, str] - item: List[RequestItem] variable: List[Variable] - -def create_env_file(path: Path, env_filename: str, variables: List[Variable]) -> Path: - env_content: List[str] = [] - - for var in variables: - env_content.append(f"{transform_variables(var.key)}={var.value}") - - env_file = path / env_filename - env_file.write_text("\n".join(env_content)) - return env_file - - -def generate_directory_structure( - items: List[RequestItem], current_path: str = "", base_path: Path = Path("") -) -> List[str]: - directories = [] - for item in items: - if item.item is not None: - folder_name = item.name - new_path = f"{current_path}/{folder_name}" if current_path else folder_name - full_path = Path(base_path) / new_path - os.makedirs(str(full_path), exist_ok=True) - directories.append(str(full_path)) - generate_directory_structure(item.item, new_path, base_path) - if item.request is not None: - request_name = re.sub(r"[^A-Za-z0-9\.]+", "", item.name) - file_name = f"{request_name}.posting.yaml" - full_path = Path(base_path) / current_path / file_name - create_request_file(full_path, item) - return directories + item: List[RequestItem] # Converts variable names like userId to $USER_ID, or user-id to $USER_ID -def transform_variables(string): +def sanitize_variables(string): underscore_case = re.sub(r"(? Path: + env_content: List[str] = [] - if request_data.request is not None: - yaml_content["method"] = request_data.request.method + for var in variables: + env_content.append(f"{sanitize_variables(var.key)}={var.value}") - if request_data.request.header is not None: - yaml_content["headers"] = [ - {"name": header.key, "value": header.value} - for header in request_data.request.header - ] + env_file = path / env_filename + env_file.write_text("\n".join(env_content)) + return env_file - if request_data.request.url is not None: - if isinstance(request_data.request.url, Url): - yaml_content["url"] = transform_url(request_data.request.url.raw) - if request_data.request.url.query is not None: - yaml_content["params"] = [ - {"name": param.key, "value": transform_url(param.value)} - for param in request_data.request.url.query - ] - else: - yaml_content["url"] = transform_url((request_data.request.url)) - if request_data.request.description is not None: - yaml_content["description"] = request_data.request.description +def import_requests( + items: List[RequestItem], base_path: Path = Path("") +) -> List[RequestModel]: + requests: List[RequestModel] = [] + for item in items: + if item.item is not None: + requests = requests + import_requests(item.item, base_path) + if item.request is not None: + file_name = re.sub(r"[^A-Za-z0-9\.]+", "", item.name) + requests.append(format_request(file_name, item.request)) + + return requests + + +def format_request(name: str, request: PostmanRequest) -> RequestModel: + postingRequest = RequestModel( + name=name, + method=request.method, + description=request.description if request.description is not None else "", + url=sanitize_str( + request.url.raw if isinstance(request.url, Url) else request.url + ) + if request.url is not None + else "", + ) + if request.header is not None: + for header in request.header: + postingRequest.headers.append( + Header( + name=header.key, + value=header.value if header.value is not None else "", + enabled=True, + ) + ) + + if ( + request.url is not None + and isinstance(request.url, Url) + and request.url.query is not None + ): + for param in request.url.query: + postingRequest.params.append( + QueryParam( + name=param.key, + value=param.value if param.value is not None else "", + enabled=param.disabled if param.disabled is not None else False, + ) + ) + + if request.body is not None and request.body.raw is not None: if ( - request_data.request.body is not None - and request_data.request.body.raw is not None + request.body.mode == "raw" + and request.body.options is not None + and request.body.options.raw.language == "json" ): - yaml_content["body"] = { - "content": transform_url(request_data.request.body.raw) - } + postingRequest.body = RequestBody(content=sanitize_str(request.body.raw)) + elif request.body.mode == "formdata" and request.body.formdata is not None: + form_data: list[FormItem] = [ + FormItem( + name=data.key, + value=data.value if data.value is not None else "", + enabled=data.disabled is False, + ) + for data in request.body.formdata + ] + postingRequest.body = RequestBody(form_data=form_data) - # Write YAML file - with open(file_path, "w") as f: - yaml.dump(yaml_content, f, default_flow_style=False) + return postingRequest def import_postman_spec( spec_path: str | Path, output_path: str | Path | None -) -> tuple[Collection, Path, List[str]]: +) -> Collection: console = Console() console.print(f"Importing Postman spec from {spec_path!r}.") @@ -166,15 +192,14 @@ def import_postman_spec( if output_path is not None: base_dir = Path(output_path) if isinstance(output_path, str) else output_path - console.print(f"Output path: {output_path!r}") + console.print(f"Output path: {str(base_dir)!r}") env_file = create_env_file(base_dir, f"{info.title}.env", spec.variable) console.print(f"Created environment file {str(env_file)!r}.") - main_collection = Collection(path=spec_path.parent, name="Postman Test") + main_collection = Collection(path=spec_path.parent, name=info.title) + main_collection.readme = main_collection.generate_readme(info) - # Create the directory structure like Postman's request folders - directories = generate_directory_structure(spec.item, base_path=base_dir) - console.print("Finished importing postman collection.") + main_collection.requests = import_requests(spec.item, base_dir) - return main_collection, env_file, directories + return main_collection diff --git a/tests/test_postman_import.py b/tests/test_postman_import.py index f1fa659a..38cd23fe 100644 --- a/tests/test_postman_import.py +++ b/tests/test_postman_import.py @@ -1,18 +1,10 @@ -import pytest -from pathlib import Path import json -import tempfile -import shutil -from posting.importing.postman import ( - Variable, - RequestItem, - PostmanRequest, - Url, - import_postman_spec, - create_env_file, - generate_directory_structure, - create_request_file, -) +from pathlib import Path +from unittest.mock import patch, mock_open + +import pytest +from posting.importing.postman import import_postman_spec, PostmanCollection +from posting.collection import Collection @pytest.fixture @@ -23,86 +15,52 @@ def sample_postman_spec(): "description": "A test API", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", }, - "variable": [{"key": "base_url", "value": "https://api.example.com"}], + "variable": [{"key": "baseUrl", "value": "https://api.example.com"}], "item": [ { - "name": "Users", - "item": [ - { - "name": "Get User", - "request": { - "method": "GET", - "url": { - "raw": "{{base_url}}/users/1", - "query": [{"key": "include", "value": "posts"}], - }, - "header": [{"key": "Accept", "value": "application/json"}], - }, - } - ], + "name": "Get Users", + "request": { + "method": "GET", + "url": { + "raw": "{{baseUrl}}/users", + "host": ["{{baseUrl}}"], + "path": ["users"], + }, + }, } ], } @pytest.fixture -def temp_dir(): - temp_dir = tempfile.mkdtemp() - yield Path(temp_dir) - shutil.rmtree(temp_dir) - - -def test_import_postman_spec(sample_postman_spec, temp_dir): - spec_file = temp_dir / "test_spec.json" - with open(spec_file, "w") as f: - json.dump(sample_postman_spec, f) - - collection, env_file, directories = import_postman_spec(spec_file, temp_dir) - - assert collection.name == "Postman Test" - assert env_file.name == "Test API.env" - assert len(directories) == 1 - assert directories[0].endswith("Users") - - -def test_create_env_file(temp_dir): - variables = [Variable(key="base_url", value="https://api.example.com")] - env_file = create_env_file(temp_dir, "test.env", variables) - - assert env_file.exists() - assert env_file.read_text() == "'base_url'='https://api.example.com'" - - -def test_generate_directory_structure(temp_dir): - request_obj = PostmanRequest(method="GET", url=Url(raw="{{base_url}}/users/1")) - request_item = RequestItem(name="Get User", request=request_obj) - item = RequestItem(name="Users", item=[request_item]) - items = [item] - - directories = generate_directory_structure(items, base_path=temp_dir) - - assert len(directories) == 1 - assert directories[0].endswith("Users") - assert (temp_dir / "Users" / "Get User.posting.yaml").exists() - - -def test_create_request_file(temp_dir): - request_obj = PostmanRequest( - method="GET", - url=Url( - raw="{{base_url}}/users/1", query=[Variable(key="include", value="posts")] - ), - header=[Variable(key="Accept", value="application/json")], - ) - request_data = RequestItem(name="Get User", request=request_obj) - - file_path = temp_dir / "test_request.posting.yaml" - create_request_file(file_path, request_data) - - assert file_path.exists() - content = file_path.read_text() - assert "name: Get User" in content - assert "method: GET" in content - assert "url: '{{base_url}}/users/1'" in content - assert "headers:" in content - assert "params:" in content +def mock_spec_file(sample_postman_spec): + return mock_open(read_data=json.dumps(sample_postman_spec)) + + +def test_import_postman_spec(sample_postman_spec, mock_spec_file): + spec_path = Path("/path/to/spec.json") + output_path = Path("/path/to/output") + + with patch("builtins.open", mock_spec_file), patch( + "posting.importing.postman.create_env_file" + ) as mock_create_env, patch( + "posting.collection.Collection.generate_readme" + ) as mock_generate_readme: + mock_create_env.return_value = output_path / "Test API.env" + mock_generate_readme.return_value = "# Test API" + + result = import_postman_spec(spec_path, output_path) + + assert isinstance(result, Collection) + assert result.name == "Test API" + assert len(result.requests) == 1 + assert result.requests[0].name == "GetUsers" + assert result.requests[0].method == "GET" + assert result.requests[0].url == "$BASE_URL/users" + + mock_create_env.assert_called_once_with( + output_path, + "Test API.env", + PostmanCollection(**sample_postman_spec).variable, + ) + mock_generate_readme.assert_called_once()