pytest-httpchain 0.1.1__py3-none-any.whl

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.
File without changes
@@ -0,0 +1,139 @@
1
+ """Test carrier class for HTTP chain test execution.
2
+
3
+ The Carrier class manages the test lifecycle and infrastructure:
4
+ - HTTP session initialization and cleanup
5
+ - Global context state management
6
+ - Test flow control (abort handling)
7
+ - Integration with pytest (skip, fail)
8
+
9
+ The actual HTTP execution and data processing is delegated to stage_executor.
10
+ """
11
+
12
+ import logging
13
+ from typing import Any, ClassVar
14
+
15
+ import pytest
16
+ import pytest_httpchain_templates.substitution
17
+ import requests
18
+ from pydantic import ValidationError
19
+ from pytest_httpchain_models.entities import Scenario, Stage
20
+ from pytest_httpchain_templates.exceptions import TemplatesError
21
+ from pytest_httpchain_userfunc.auth import call_auth_function
22
+
23
+ from . import stage_executor
24
+ from .exceptions import StageExecutionError
25
+ from .helpers import call_user_function
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class Carrier:
31
+ """Test carrier class that integrates HTTP chain test execution.
32
+
33
+ This base class is subclassed dynamically by carrier_factory to create
34
+ test classes with scenario-specific test methods. It manages the shared
35
+ state and execution flow for all stages in a test scenario.
36
+
37
+ Attributes:
38
+ _scenario: The test scenario configuration
39
+ _session: Shared HTTP session for all stages
40
+ _data_context: Global context shared across all stages
41
+ _aborted: Flag indicating if test flow should be aborted
42
+ """
43
+
44
+ _scenario: ClassVar[Scenario]
45
+ _session: ClassVar[requests.Session | None] = None
46
+ _data_context: ClassVar[dict[str, Any]] = {}
47
+ _aborted: ClassVar[bool] = False
48
+
49
+ @classmethod
50
+ def setup_class(cls) -> None:
51
+ """Initialize the HTTP session and data context.
52
+
53
+ Called once before any test methods in the class are executed.
54
+ Sets up:
55
+ - Empty data context for variable storage
56
+ - HTTP session with SSL and authentication configuration
57
+
58
+ Note:
59
+ Authentication can be configured at scenario level and will
60
+ be applied to all requests unless overridden at stage level.
61
+ """
62
+ cls._data_context = {}
63
+ cls._session = requests.Session()
64
+
65
+ # Configure SSL settings
66
+ cls._session.verify = cls._scenario.ssl.verify
67
+ if cls._scenario.ssl.cert is not None:
68
+ cls._session.cert = cls._scenario.ssl.cert
69
+
70
+ # Configure authentication
71
+ if cls._scenario.auth:
72
+ resolved_auth = pytest_httpchain_templates.substitution.walk(cls._scenario.auth, cls._data_context)
73
+ auth_instance = call_user_function(resolved_auth, call_auth_function)
74
+ cls._session.auth = auth_instance
75
+
76
+ @classmethod
77
+ def teardown_class(cls) -> None:
78
+ """Clean up the HTTP session and reset state.
79
+
80
+ Called once after all test methods in the class have been executed.
81
+ Ensures proper cleanup of resources and state reset for next test class.
82
+ """
83
+ if cls._session:
84
+ cls._session.close()
85
+ cls._session = None
86
+ cls._data_context.clear()
87
+ cls._aborted = False
88
+
89
+ @classmethod
90
+ def execute_stage(cls, stage_template: Stage, fixture_kwargs: dict[str, Any]) -> None:
91
+ """Execute a test stage with abort handling and error management.
92
+
93
+ This method is called for each stage in the scenario. It handles:
94
+ - Checking abort status and skipping if needed
95
+ - Executing the stage via stage_executor
96
+ - Updating global context with saved variables
97
+ - Setting abort flag on errors
98
+
99
+ Args:
100
+ stage_template: The stage configuration containing request/response definitions
101
+ fixture_kwargs: Dictionary of pytest fixture values injected for this stage
102
+
103
+ Raises:
104
+ pytest.skip: If flow is aborted and stage doesn't have always_run=True
105
+ pytest.fail: If stage execution fails with an error
106
+
107
+ Note:
108
+ Sets cls._aborted to True on failure, causing subsequent stages
109
+ to be skipped unless they have always_run=True.
110
+ """
111
+ try:
112
+ # Check abort status
113
+ if cls._aborted and not stage_template.always_run:
114
+ pytest.skip(reason="Flow aborted")
115
+
116
+ # Verify session is initialized
117
+ if cls._session is None:
118
+ raise RuntimeError("Session not initialized - setup_class was not called")
119
+
120
+ # Execute stage and get variables to save globally
121
+ context_updates = stage_executor.execute_stage(
122
+ stage_template=stage_template,
123
+ scenario=cls._scenario,
124
+ session=cls._session,
125
+ global_context=cls._data_context, # Pass current global state
126
+ fixture_kwargs=fixture_kwargs,
127
+ )
128
+
129
+ # Merge returned updates into global context for next stages
130
+ cls._data_context.update(context_updates)
131
+
132
+ except (
133
+ TemplatesError,
134
+ StageExecutionError,
135
+ ValidationError,
136
+ ) as e:
137
+ logger.exception(str(e))
138
+ cls._aborted = True
139
+ pytest.fail(reason=str(e), pytrace=False)
@@ -0,0 +1,92 @@
1
+ """Factory for creating dynamic test classes.
2
+
3
+ This module provides functionality to dynamically generate pytest test classes
4
+ from JSON scenario definitions. Each scenario becomes a test class with one
5
+ test method per stage.
6
+ """
7
+
8
+ import inspect
9
+ import logging
10
+ from typing import Any
11
+
12
+ import pytest
13
+ from pytest_httpchain_models.entities import Scenario, Stage
14
+ from simpleeval import EvalWithCompoundTypes
15
+
16
+ from .carrier import Carrier
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def create_test_class(scenario: Scenario, class_name: str) -> type[Carrier]:
22
+ """Create a dynamic test class for the given scenario.
23
+
24
+ This factory function generates a pytest test class with:
25
+ - One test method per stage in the scenario
26
+ - Automatic fixture injection based on stage requirements
27
+ - Marker application (order, skip, xfail, etc.)
28
+ - Shared session and context management
29
+
30
+ The generated class structure:
31
+ - Inherits from Carrier base class
32
+ - Has test_0_<stage_name>, test_1_<stage_name>, etc. methods
33
+ - Each method requests fixtures defined in stage and scenario
34
+ - Methods are ordered using pytest-order plugin
35
+
36
+ Args:
37
+ scenario: Validated scenario configuration containing stages
38
+ class_name: Name for the generated test class
39
+
40
+ Returns:
41
+ A Carrier subclass with test methods for each stage
42
+
43
+ Example:
44
+ >>> scenario = Scenario.model_validate(test_data)
45
+ >>> TestClass = create_test_class(scenario, Path("test.json"), "TestAPI")
46
+ >>> # TestClass will have methods: test_0_stage1, test_1_stage2, etc.
47
+ """
48
+ # Create custom Carrier class with scenario bound
49
+ CustomCarrier = type(
50
+ class_name,
51
+ (Carrier,),
52
+ {
53
+ "_scenario": scenario,
54
+ "_session": None,
55
+ "_data_context": {},
56
+ "_aborted": False,
57
+ },
58
+ )
59
+
60
+ # Add stage methods dynamically
61
+ for i, stage in enumerate(scenario.stages):
62
+ # Create stage method - using default argument to capture stage
63
+ def stage_method(self, *, _stage: Stage = stage, **fixture_kwargs: dict[str, Any]) -> None:
64
+ """Execute a single stage of the test scenario.
65
+
66
+ Auto-generated method that executes one stage of the HTTP chain test.
67
+
68
+ Args:
69
+ **fixture_kwargs: Pytest fixtures requested by this stage
70
+ """
71
+ CustomCarrier.execute_stage(_stage, fixture_kwargs)
72
+
73
+ # Set up method signature with fixtures
74
+ all_fixtures: list[str] = ["self"] + stage.fixtures + scenario.fixtures
75
+ stage_method.__signature__ = inspect.Signature([inspect.Parameter(name, inspect.Parameter.POSITIONAL_OR_KEYWORD) for name in all_fixtures])
76
+
77
+ # Apply markers
78
+ all_marks: list[str] = [f"order({i})"] + stage.marks
79
+ evaluator = EvalWithCompoundTypes(names={"pytest": pytest})
80
+ for mark_str in all_marks:
81
+ try:
82
+ marker = evaluator.eval(f"pytest.mark.{mark_str}")
83
+ if marker:
84
+ stage_method = marker(stage_method)
85
+ except Exception as e:
86
+ logger.warning(f"Failed to create marker '{mark_str}': {e}")
87
+
88
+ # Add method to class with descriptive name
89
+ method_name = f"test_{i}_{stage.name}"
90
+ setattr(CustomCarrier, method_name, stage_method)
91
+
92
+ return CustomCarrier
@@ -0,0 +1,10 @@
1
+ """Constants for pytest-httpchain plugin."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class ConfigOptions(StrEnum):
7
+ """Configuration option names for the pytest-httpchain plugin."""
8
+
9
+ SUFFIX = "suffix"
10
+ REF_PARENT_TRAVERSAL_DEPTH = "ref_parent_traversal_depth"
@@ -0,0 +1,77 @@
1
+ """Context management for HTTP chain test execution.
2
+
3
+ This module handles the preparation and management of execution contexts,
4
+ maintaining the separation between global and local state.
5
+ """
6
+
7
+ from collections import ChainMap
8
+ from typing import Any
9
+
10
+ import pytest_httpchain_templates.substitution
11
+ from pytest_httpchain_models.entities import Scenario, Stage
12
+
13
+
14
+ def prepare_data_context(
15
+ scenario: Scenario,
16
+ stage_template: Stage,
17
+ global_context: dict[str, Any],
18
+ fixture_kwargs: dict[str, Any],
19
+ ) -> ChainMap[str, Any]:
20
+ """Prepare the complete data context for stage execution.
21
+
22
+ Uses ChainMap for efficient layered context management with lazy evaluation.
23
+ No copying occurs - all layers share references to original data.
24
+
25
+ Merges contexts in order of precedence (later overrides earlier):
26
+ 1. Global context (shared across all stages) - base layer
27
+ 2. Fixture values (from pytest fixtures)
28
+ 3. Scenario variables (from scenario.vars)
29
+ 4. Stage variables (from stage.vars) - top layer
30
+
31
+ Each level can reference variables from previous levels in templates.
32
+
33
+ Args:
34
+ scenario: The scenario configuration
35
+ stage_template: The stage being executed
36
+ global_context: Shared context from previous stages
37
+ fixture_kwargs: Pytest fixture values for this stage
38
+
39
+ Returns:
40
+ ChainMap with layered context for efficient lookups
41
+
42
+ Note:
43
+ Returns a ChainMap for full performance benefits:
44
+ - No data copying
45
+ - Lazy evaluation (only accesses what's needed)
46
+ - Memory efficient (shares references)
47
+ - O(1) for most lookups
48
+ """
49
+ # Build layers incrementally - each layer can reference previous ones
50
+ # Template substitution now works directly with ChainMap
51
+
52
+ # Layer 1: Base context (global + fixtures)
53
+ base_context = ChainMap(fixture_kwargs, global_context)
54
+
55
+ # Layer 2: Scenario variables (can reference base)
56
+ scenario_vars = {}
57
+ if scenario.vars:
58
+ scenario_vars = pytest_httpchain_templates.substitution.walk(
59
+ scenario.vars,
60
+ base_context, # Pass ChainMap directly
61
+ )
62
+
63
+ # Layer 3: Stage variables (can reference base + scenario)
64
+ # Process stage vars incrementally so they can reference each other
65
+ stage_vars = {}
66
+ if stage_template.vars:
67
+ context_with_scenario = ChainMap({}, scenario_vars, fixture_kwargs, global_context)
68
+ for key, value in stage_template.vars.items():
69
+ resolved_value = pytest_httpchain_templates.substitution.walk(value, context_with_scenario)
70
+ stage_vars[key] = resolved_value
71
+ # Add resolved var to context so next vars can reference it
72
+ context_with_scenario.maps[0][key] = resolved_value
73
+
74
+ # Create final context with proper precedence order
75
+ # Stage vars override scenario vars, which override fixtures, which override global
76
+ # Returns ChainMap for full performance benefits
77
+ return ChainMap(stage_vars, scenario_vars, fixture_kwargs, global_context)
@@ -0,0 +1,46 @@
1
+ """Exception classes for HTTP chain test execution.
2
+
3
+ This module defines the exception hierarchy used throughout the
4
+ pytest-httpchain test execution flow.
5
+ """
6
+
7
+
8
+ class StageExecutionError(Exception):
9
+ """Base exception for all stage execution errors.
10
+
11
+ This is the base class for all exceptions that can occur during
12
+ stage execution. Catching this will catch all stage-related errors.
13
+ """
14
+
15
+
16
+ class RequestError(StageExecutionError):
17
+ """Error during HTTP request preparation or execution.
18
+
19
+ Raised when:
20
+ - Request preparation fails (auth, file opening, etc.)
21
+ - HTTP request times out
22
+ - Connection errors occur
23
+ - Other request-related issues
24
+ """
25
+
26
+
27
+ class SaveError(StageExecutionError):
28
+ """Error during response processing (save operations).
29
+
30
+ Raised when:
31
+ - JMESPath expression fails
32
+ - User save function fails
33
+ - Variable extraction fails
34
+ """
35
+
36
+
37
+ class VerificationError(StageExecutionError):
38
+ """Error during response verification.
39
+
40
+ Raised when:
41
+ - Status code doesn't match expected
42
+ - Headers don't match expected
43
+ - Response body validation fails
44
+ - User verify function returns False
45
+ - JSON schema validation fails
46
+ """
@@ -0,0 +1,43 @@
1
+ """Helper functions for common patterns in pytest-httpchain."""
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any
5
+
6
+ from pytest_httpchain_models.entities import UserFunctionCall, UserFunctionKwargs
7
+
8
+
9
+ def call_user_function(
10
+ model: UserFunctionCall,
11
+ call_function: Callable,
12
+ *args: Any,
13
+ **extra_kwargs: Any,
14
+ ) -> Any:
15
+ """Generic helper to call user functions from UserFunctionCall model.
16
+
17
+ This helper eliminates the repeated pattern of checking whether a UserFunctionCall
18
+ is a UserFunctionName or UserFunctionKwargs and calling the appropriate function.
19
+
20
+ Args:
21
+ model: Either UserFunctionName or UserFunctionKwargs
22
+ call_function: The specific function caller (call_save_function, call_verify_function, etc.)
23
+ *args: Positional arguments to pass to call_function (e.g., response object)
24
+ **extra_kwargs: Additional keyword arguments for call_function
25
+
26
+ Returns:
27
+ Result from the called function
28
+
29
+ Example:
30
+ # Instead of:
31
+ if isinstance(func_item, UserFunctionKwargs):
32
+ result = call_save_function(func_item.function.root, response, **func_item.kwargs)
33
+ else:
34
+ result = call_save_function(func_item.root, response)
35
+
36
+ # Use:
37
+ result = call_user_function(func_item, call_save_function, response)
38
+ """
39
+ if isinstance(model, UserFunctionKwargs):
40
+ kwargs = {**extra_kwargs, **model.kwargs}
41
+ return call_function(model.function.root, *args, **kwargs)
42
+ else: # UserFunctionName
43
+ return call_function(model.root, *args, **extra_kwargs)
@@ -0,0 +1,192 @@
1
+ """Pytest plugin for HTTP chain testing.
2
+
3
+ This module provides the pytest plugin hooks and collection logic for
4
+ discovering and executing HTTP chain tests from JSON files.
5
+ """
6
+
7
+ import logging
8
+ import re
9
+ import types
10
+ from collections.abc import Iterable
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import pytest
15
+ import pytest_httpchain_jsonref.loader
16
+ from _pytest import config, nodes, python, reports, runner
17
+ from _pytest.config import argparsing
18
+ from pydantic import ValidationError
19
+ from pytest_httpchain_jsonref.exceptions import ReferenceResolverError
20
+ from pytest_httpchain_models.entities import Scenario
21
+ from simpleeval import EvalWithCompoundTypes
22
+
23
+ from pytest_httpchain.constants import ConfigOptions
24
+
25
+ from .carrier_factory import create_test_class
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class JsonModule(python.Module):
31
+ """JSON test module that collects and executes HTTP chain tests.
32
+
33
+ This class extends pytest's Module to handle JSON test files containing
34
+ HTTP chain test scenarios. It loads, validates, and converts JSON test
35
+ definitions into executable pytest test classes.
36
+ """
37
+
38
+ def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
39
+ """Collect test items from a JSON module.
40
+
41
+ This method:
42
+ 1. Loads the JSON file with reference resolution
43
+ 2. Validates the test scenario against the schema
44
+ 3. Creates a dynamic test class using the factory
45
+ 4. Yields the test class for pytest to execute
46
+
47
+ Yields:
48
+ python.Class: A pytest Class node containing test methods
49
+
50
+ Raises:
51
+ Collector.CollectError: If JSON loading or validation fails
52
+ """
53
+ # Load and validate the test scenario from JSON
54
+ ref_parent_traversal_depth = int(self.config.getini(ConfigOptions.REF_PARENT_TRAVERSAL_DEPTH))
55
+
56
+ try:
57
+ test_data = pytest_httpchain_jsonref.loader.load_json(
58
+ self.path,
59
+ max_parent_traversal_depth=ref_parent_traversal_depth,
60
+ )
61
+ except ReferenceResolverError as e:
62
+ raise nodes.Collector.CollectError("Cannot load JSON file") from e
63
+
64
+ try:
65
+ scenario = Scenario.model_validate(test_data)
66
+ except ValidationError as e:
67
+ raise nodes.Collector.CollectError("Cannot parse test scenario") from e
68
+
69
+ # Create test class using factory
70
+ CarrierClass = create_test_class(scenario, self.name)
71
+
72
+ # Create pytest Class node
73
+ dummy_module = types.ModuleType("generated")
74
+ setattr(dummy_module, self.name, CarrierClass)
75
+ self._getobj = lambda: dummy_module
76
+
77
+ json_class = python.Class.from_parent(
78
+ self,
79
+ path=self.path,
80
+ name=self.name,
81
+ obj=CarrierClass,
82
+ )
83
+
84
+ # Apply scenario-level markers
85
+ evaluator = EvalWithCompoundTypes(names={"pytest": pytest})
86
+ for mark_str in scenario.marks:
87
+ try:
88
+ marker = evaluator.eval(f"pytest.mark.{mark_str}")
89
+ if marker:
90
+ json_class.add_marker(marker)
91
+ except Exception as e:
92
+ logger.warning(f"Failed to create marker '{mark_str}': {e}")
93
+
94
+ yield json_class
95
+
96
+
97
+ def pytest_addoption(parser: argparsing.Parser) -> None:
98
+ """Add command-line options for the plugin.
99
+
100
+ Registers configuration options that can be set in pytest.ini:
101
+ - httpchain_suffix: File suffix for test files (default: "http")
102
+ - httpchain_ref_parent_traversal_depth: Max parent directory traversals in $ref paths
103
+
104
+ Args:
105
+ parser: Pytest's argument parser to add options to
106
+ """
107
+ parser.addini(
108
+ name=ConfigOptions.SUFFIX,
109
+ help="File suffix for HTTP test files.",
110
+ type="string",
111
+ default="http",
112
+ )
113
+ parser.addini(
114
+ name=ConfigOptions.REF_PARENT_TRAVERSAL_DEPTH,
115
+ help="Maximum number of parent directory traversals allowed in $ref paths.",
116
+ type="string",
117
+ default="3",
118
+ )
119
+
120
+
121
+ def pytest_configure(config: config.Config) -> None:
122
+ """Validate configuration settings.
123
+
124
+ Ensures that configuration values are valid:
125
+ - Suffix must be alphanumeric with underscores/hyphens, max 32 chars
126
+ - Reference traversal depth must be non-negative
127
+
128
+ Args:
129
+ config: Pytest configuration object
130
+
131
+ Raises:
132
+ ValueError: If configuration values are invalid
133
+ """
134
+ suffix = str(config.getini(ConfigOptions.SUFFIX))
135
+ if not re.match(r"^[a-zA-Z0-9_-]{1,32}$", suffix):
136
+ raise ValueError("suffix must contain only alphanumeric characters, underscores, hyphens, and be ≤32 chars")
137
+
138
+ ref_parent_traversal_depth = int(config.getini(ConfigOptions.REF_PARENT_TRAVERSAL_DEPTH))
139
+ if ref_parent_traversal_depth < 0:
140
+ raise ValueError("Maximum number of parent directory traversals must be non-negative")
141
+
142
+
143
+ def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> nodes.Collector | None:
144
+ """Collect JSON test files matching the configured pattern.
145
+
146
+ This hook is called by pytest for each file in the test directory.
147
+ It checks if the file matches the pattern: test_<name>.<suffix>.json
148
+ where suffix is configurable (default: "http").
149
+
150
+ Args:
151
+ file_path: Path to the file being considered for collection
152
+ parent: The parent collector node
153
+
154
+ Returns:
155
+ JsonModule collector if file matches pattern, None otherwise
156
+
157
+ Example:
158
+ For suffix="http", these files would be collected:
159
+ - test_api.http.json
160
+ - test_user_flow.http.json
161
+
162
+ Configuration:
163
+ The suffix can be configured in pytest.ini:
164
+ [tool.pytest.ini_options]
165
+ httpchain_suffix = "api" # Changes pattern to test_*.api.json
166
+ """
167
+ suffix: str = parent.config.getini(ConfigOptions.SUFFIX)
168
+ pattern = re.compile(rf"^test_(?P<name>.+)\.{re.escape(suffix)}\.json$")
169
+ file_match = pattern.match(file_path.name)
170
+ if file_match:
171
+ return JsonModule.from_parent(parent, path=file_path, name=file_match.group("name"))
172
+ return None
173
+
174
+
175
+ @pytest.hookimpl(hookwrapper=True)
176
+ def pytest_runtest_makereport(item: nodes.Item, call: runner.CallInfo[Any]) -> Any:
177
+ """Add custom sections to test reports.
178
+
179
+ This hook adds additional information to test reports that can be
180
+ displayed in pytest output or used by other plugins.
181
+
182
+ Args:
183
+ item: The test item being reported on
184
+ call: Information about the test call
185
+
186
+ Yields:
187
+ The report with additional sections added
188
+ """
189
+ outcome = yield
190
+ report: reports.TestReport = outcome.get_result()
191
+ if call.when == "call":
192
+ report.sections.append(("call_title", "call_value"))
@@ -0,0 +1,102 @@
1
+ """HTTP request preparation and execution for chain tests.
2
+
3
+ This module handles the preparation of HTTP requests from test configurations
4
+ and their execution using the requests library.
5
+ """
6
+
7
+ from contextlib import ExitStack
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import requests
12
+ from pytest_httpchain_models.entities import (
13
+ FilesBody,
14
+ FormBody,
15
+ JsonBody,
16
+ RawBody,
17
+ XmlBody,
18
+ )
19
+ from pytest_httpchain_models.entities import (
20
+ Request as RequestModel,
21
+ )
22
+ from pytest_httpchain_userfunc.auth import call_auth_function
23
+
24
+ from .exceptions import RequestError
25
+ from .helpers import call_user_function
26
+
27
+
28
+ def prepare_and_execute(
29
+ session: requests.Session,
30
+ request_model: RequestModel,
31
+ ) -> requests.Response:
32
+ """Prepare and execute an HTTP request.
33
+
34
+ This function combines preparation and execution to avoid unnecessary
35
+ complexity. It handles authentication, different body types, and file
36
+ uploads with proper resource management.
37
+
38
+ Args:
39
+ session: HTTP session to use for the request
40
+ request_model: Validated request model
41
+
42
+ Returns:
43
+ HTTP response object
44
+
45
+ Raises:
46
+ RequestError: If request preparation or execution fails
47
+ """
48
+
49
+ # Base request kwargs
50
+ kwargs: dict[str, Any] = {
51
+ "method": request_model.method.value,
52
+ "url": str(request_model.url),
53
+ "headers": request_model.headers,
54
+ "params": request_model.params,
55
+ "timeout": request_model.timeout,
56
+ "allow_redirects": request_model.allow_redirects,
57
+ "verify": request_model.ssl.verify,
58
+ }
59
+
60
+ # Add SSL cert if present
61
+ if request_model.ssl.cert:
62
+ kwargs["cert"] = request_model.ssl.cert
63
+
64
+ # Configure auth if present
65
+ if request_model.auth:
66
+ try:
67
+ kwargs["auth"] = call_user_function(request_model.auth, call_auth_function)
68
+ except Exception as e:
69
+ raise RequestError("Failed to configure authentication") from e
70
+
71
+ # Handle different body types
72
+ match request_model.body:
73
+ case None:
74
+ pass
75
+ case JsonBody(json=data):
76
+ kwargs["json"] = data
77
+ case FormBody(form=data) | XmlBody(xml=data) | RawBody(raw=data):
78
+ kwargs["data"] = data
79
+ case FilesBody(files=file_paths):
80
+ # Handle file uploads with context manager
81
+ with ExitStack() as stack:
82
+ try:
83
+ files_dict = {}
84
+ for field_name, file_path in file_paths.items():
85
+ file_handle = stack.enter_context(open(file_path, "rb"))
86
+ files_dict[field_name] = (Path(file_path).name, file_handle)
87
+ kwargs["files"] = files_dict
88
+
89
+ return session.request(**kwargs)
90
+ except FileNotFoundError as e:
91
+ raise RequestError("File not found for upload") from e
92
+
93
+ try:
94
+ return session.request(**kwargs)
95
+ except requests.Timeout as e:
96
+ raise RequestError("HTTP request timed out") from e
97
+ except requests.ConnectionError as e:
98
+ raise RequestError("HTTP connection error") from e
99
+ except requests.RequestException as e:
100
+ raise RequestError("HTTP request failed") from e
101
+ except Exception as e:
102
+ raise RequestError("Unexpected error") from e
@@ -0,0 +1,165 @@
1
+ """Response processing and verification for HTTP chain tests.
2
+
3
+ This module handles the processing of HTTP responses including data extraction
4
+ (save operations) and verification of response content.
5
+ """
6
+
7
+ import json
8
+ import re
9
+ from collections import ChainMap
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ import jmespath
14
+ import jsonschema
15
+ import requests
16
+ from pytest_httpchain_models.entities import Save, Verify
17
+ from pytest_httpchain_models.types import check_json_schema
18
+ from pytest_httpchain_userfunc.save import call_save_function
19
+ from pytest_httpchain_userfunc.verify import call_verify_function
20
+
21
+ from .exceptions import SaveError, VerificationError
22
+ from .helpers import call_user_function
23
+
24
+
25
+ def process_save_step(
26
+ save_model: Save,
27
+ response: requests.Response,
28
+ ) -> dict[str, Any]:
29
+ """Process a save step and return variables to be saved to global context.
30
+
31
+ Extracts data from the response using:
32
+ - JMESPath expressions for JSON responses
33
+ - User-defined save functions for custom extraction
34
+
35
+ Args:
36
+ save_model: Validated Save model
37
+ response: HTTP response object
38
+
39
+ Returns:
40
+ Dictionary of variables to add to global context
41
+
42
+ Raises:
43
+ ResponseError: If variable extraction fails
44
+
45
+ Note:
46
+ Save functions must conform to the SaveFunction protocol,
47
+ accepting a response and returning a dict[str, Any].
48
+ """
49
+ result: dict[str, Any] = {}
50
+
51
+ # Extract JSON only if we need it for JMESPath expressions
52
+ if len(save_model.vars) > 0:
53
+ try:
54
+ response_json = response.json()
55
+ except (requests.JSONDecodeError, UnicodeDecodeError) as e:
56
+ raise SaveError("Cannot extract variables: response is not valid JSON") from e
57
+
58
+ for var_name, jmespath_expr in save_model.vars.items():
59
+ try:
60
+ saved_value = jmespath.search(jmespath_expr, response_json)
61
+ result[var_name] = saved_value
62
+ except jmespath.exceptions.JMESPathError as e:
63
+ raise SaveError(f"Error saving variable {var_name}") from e
64
+
65
+ for func_item in save_model.functions:
66
+ try:
67
+ func_result = call_user_function(func_item, call_save_function, response)
68
+ result.update(func_result)
69
+ except Exception as e:
70
+ raise SaveError(f"Error calling user function {func_item}") from e
71
+
72
+ return result
73
+
74
+
75
+ def process_verify_step(
76
+ verify_model: Verify,
77
+ local_context: ChainMap[str, Any],
78
+ response: requests.Response,
79
+ ) -> None:
80
+ """Process a verify step and raise errors if verification fails.
81
+
82
+ Performs various verifications on the response:
83
+ - Status code matching
84
+ - Header value matching
85
+ - Variable value matching
86
+ - JSON schema validation
87
+ - Body content checks (contains/not_contains/matches/not_matches)
88
+ - User-defined verify functions
89
+
90
+ Args:
91
+ verify_model: Validated Verify model
92
+ local_context: Current execution context
93
+ response: HTTP response object
94
+
95
+ Raises:
96
+ VerificationError: If any verification fails
97
+
98
+ Note:
99
+ Verify functions must conform to the VerifyFunction protocol,
100
+ accepting a response and returning a bool.
101
+ """
102
+
103
+ if verify_model.status and response.status_code != verify_model.status.value:
104
+ raise VerificationError(f"Status code doesn't match: expected {verify_model.status.value}, got {response.status_code}")
105
+
106
+ for header_name, expected_value in verify_model.headers.items():
107
+ if response.headers.get(header_name) != expected_value:
108
+ raise VerificationError(f"Header '{header_name}' doesn't match: expected {expected_value}, got {response.headers.get(header_name)}")
109
+
110
+ for var_name, expected_value in verify_model.vars.items():
111
+ if var_name not in local_context:
112
+ raise VerificationError(f"Var '{var_name}' not found in data context")
113
+ if local_context[var_name] != expected_value:
114
+ raise VerificationError(f"Var '{var_name}' verification failed: expected {expected_value}, got {local_context[var_name]}")
115
+
116
+ for func_item in verify_model.functions:
117
+ try:
118
+ result = call_user_function(func_item, call_verify_function, response)
119
+
120
+ if not result:
121
+ raise VerificationError(f"Function '{func_item}' verification failed")
122
+
123
+ except Exception as e:
124
+ raise VerificationError(f"Error calling user function '{func_item}'") from e
125
+
126
+ if verify_model.body.schema:
127
+ schema = verify_model.body.schema
128
+ if isinstance(schema, str | Path):
129
+ schema_path = Path(schema)
130
+ try:
131
+ schema = json.loads(schema_path.read_text())
132
+ check_json_schema(schema)
133
+ except (OSError, json.JSONDecodeError) as e:
134
+ raise VerificationError(f"Error reading body schema file '{schema_path}'") from e
135
+ except jsonschema.SchemaError as e:
136
+ raise VerificationError(f"Invalid JSON Schema in file '{schema_path}': {e.message}") from e
137
+
138
+ # Extract JSON for schema validation
139
+ try:
140
+ response_json = response.json()
141
+ except (requests.JSONDecodeError, UnicodeDecodeError) as e:
142
+ raise VerificationError("Cannot validate schema: response is not valid JSON") from e
143
+
144
+ try:
145
+ jsonschema.validate(instance=response_json, schema=schema)
146
+ except jsonschema.ValidationError as e:
147
+ raise VerificationError("Body schema validation failed") from e
148
+ except jsonschema.SchemaError as e:
149
+ raise VerificationError("Invalid body validation schema") from e
150
+
151
+ for substring in verify_model.body.contains:
152
+ if substring not in response.text:
153
+ raise VerificationError(f"Body doesn't contain '{substring}'")
154
+
155
+ for substring in verify_model.body.not_contains:
156
+ if substring in response.text:
157
+ raise VerificationError(f"Body contains '{substring}' while it shouldn't")
158
+
159
+ for pattern in verify_model.body.matches:
160
+ if not re.search(pattern, response.text):
161
+ raise VerificationError(f"Body doesn't match '{pattern}'")
162
+
163
+ for pattern in verify_model.body.not_matches:
164
+ if re.search(pattern, response.text):
165
+ raise VerificationError(f"Body matches '{pattern}' while it shouldn't")
@@ -0,0 +1,111 @@
1
+ """Stage execution logic for HTTP chain tests.
2
+
3
+ This module orchestrates the execution of individual test stages by coordinating
4
+ between the specialized modules:
5
+
6
+ - context.py: Handles context preparation and management
7
+ - request.py: Manages HTTP request building and execution
8
+ - response.py: Processes responses (save and verify operations)
9
+ - exceptions.py: Defines the exception hierarchy
10
+
11
+ The main responsibility of this module is to coordinate the flow:
12
+ 1. Build local context using context module
13
+ 2. Prepare and execute request using request module
14
+ 3. Process response using response module
15
+ 4. Return updates for global context
16
+ """
17
+
18
+ import logging
19
+ from typing import Any
20
+
21
+ import pytest_httpchain_templates.substitution
22
+ import requests
23
+ from pytest_httpchain_models.entities import (
24
+ Request,
25
+ Response,
26
+ Save,
27
+ SaveStep,
28
+ Scenario,
29
+ Stage,
30
+ Verify,
31
+ VerifyStep,
32
+ )
33
+
34
+ from .context import prepare_data_context
35
+ from .request import prepare_and_execute
36
+ from .response import process_save_step, process_verify_step
37
+
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+ def execute_stage(
42
+ stage_template: Stage,
43
+ scenario: Scenario,
44
+ session: requests.Session,
45
+ global_context: dict[str, Any],
46
+ fixture_kwargs: dict[str, Any],
47
+ ) -> dict[str, Any]:
48
+ """Execute a single stage and return context updates.
49
+
50
+ This is the main entry point for stage execution. It orchestrates:
51
+ 1. Context preparation (merge global + fixtures + variables)
52
+ 2. Template substitution for all stage elements
53
+ 3. HTTP request preparation and execution
54
+ 4. Response processing (save and verify steps)
55
+ 5. Return updates for global context
56
+
57
+ Args:
58
+ stage_template: The stage definition (with templates)
59
+ scenario: The scenario configuration
60
+ session: HTTP session for requests
61
+ global_context: Shared context from previous stages (read-only)
62
+ fixture_kwargs: Values from pytest fixtures
63
+
64
+ Returns:
65
+ Context updates to be merged into global context.
66
+ Only includes variables from SaveStep operations.
67
+
68
+ Raises:
69
+ RequestError: HTTP request preparation/execution failed
70
+ ResponseError: Response processing (save) failed
71
+ VerificationError: Response verification failed
72
+
73
+ Note:
74
+ The function maintains a clear separation between global and local
75
+ context. Only SaveStep results are returned for global updates.
76
+ """
77
+ # Build local context for this stage (global + fixtures + vars)
78
+ local_context = prepare_data_context(scenario=scenario, stage_template=stage_template, global_context=global_context, fixture_kwargs=fixture_kwargs)
79
+
80
+ # Resolve stage template with complete local context
81
+ stage = pytest_httpchain_templates.substitution.walk(stage_template, local_context)
82
+
83
+ # Prepare and execute request
84
+ request_dict = pytest_httpchain_templates.substitution.walk(stage.request, local_context)
85
+ request_model = Request.model_validate(request_dict)
86
+ response = prepare_and_execute(session, request_model)
87
+
88
+ # Process response
89
+ response_dict = pytest_httpchain_templates.substitution.walk(stage.response, local_context)
90
+ response_model = Response.model_validate(response_dict)
91
+
92
+ # Track what needs to be saved to global context
93
+ global_context_updates: dict[str, Any] = {}
94
+
95
+ for step in response_model:
96
+ match step:
97
+ case SaveStep():
98
+ save_dict = pytest_httpchain_templates.substitution.walk(step.save, local_context)
99
+ save_model = Save.model_validate(save_dict)
100
+ saved_vars = process_save_step(save_model, response)
101
+ # Add saved vars as a new layer in ChainMap for subsequent steps
102
+ local_context = local_context.new_child(saved_vars)
103
+ global_context_updates.update(saved_vars)
104
+
105
+ case VerifyStep():
106
+ verify_dict = pytest_httpchain_templates.substitution.walk(step.verify, local_context)
107
+ verify_model = Verify.model_validate(verify_dict)
108
+ process_verify_step(verify_model, local_context, response)
109
+
110
+ # Return only the updates that should persist globally
111
+ return global_context_updates
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: pytest-httpchain
3
+ Version: 0.1.1
4
+ Summary: pytest plugin for HTTP testing using JSON files
5
+ Keywords: testing,pytest,requests
6
+ Author: Alexander Eresov
7
+ Author-email: Alexander Eresov <aeresov@gmail.com>
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Framework :: Pytest
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: Testing
16
+ Requires-Dist: pydantic>=2.11.7
17
+ Requires-Dist: pytest-httpchain-jsonref
18
+ Requires-Dist: pytest-httpchain-models
19
+ Requires-Dist: pytest-order>=1.3.0
20
+ Requires-Dist: rich>=13.7.0
21
+ Requires-Dist: pytest-httpchain-mcp ; extra == 'mcp'
22
+ Requires-Python: >=3.13, <4.0
23
+ Provides-Extra: mcp
24
+ Description-Content-Type: text/markdown
25
+
26
+ [![image](https://img.shields.io/pypi/v/pytest-httpchain)](https://pypi.python.org/pypi/pytest-httpchain)
27
+ [![image](https://img.shields.io/pypi/l/pytest-httpchain)](https://github.com/aeresov/pytest-httpchain/blob/main/LICENSE)
28
+ [![image](https://img.shields.io/pypi/pyversions/pytest-httpchain)](https://pypi.python.org/pypi/pytest-httpchain)
29
+
30
+ # pytest-httpchain
31
+
32
+ A pytest plugin for testing HTTP endpoints.
33
+
34
+ ## Overview
35
+
36
+ `pytest-httpchain` is an integration testing framework for HTTP APIs based on battle-hardened [requests](https://requests.readthedocs.io) lib.
37
+ It aims at helping with common HTTP API testing scenarios, where user needs to make several calls in specific order using data obtained along the way, like auth tokens or resource ids.
38
+
39
+ ## Installation
40
+
41
+ Install normally via package manager of your choice from PyPi:
42
+
43
+ ```bash
44
+ pip install pytest-httpchain
45
+ ```
46
+
47
+ or directly from Github, in case you need a particular ref:
48
+
49
+ ```bash
50
+ pip install 'git+https://github.com/aeresov/pytest-httpchain@main'
51
+ ```
52
+
53
+ ### Optional dependencies
54
+
55
+ The following optional dependencies are available:
56
+
57
+ - `mcp`: installs MCP server package and its starting script. Details in [MCP Server](#mcp-server).
58
+
59
+ ## Features
60
+
61
+ ### Pytest integration
62
+
63
+ Most of pytest magic can be used: markers, fixtures, other plugins.
64
+
65
+ > NOTE: parametrization is not yet implemented, therefore `parametrize` marker won't have any effect.
66
+
67
+ ### Declarative format
68
+
69
+ Test scenarios are written declaratively in JSON files.
70
+ `pytest-httpchain` supports JSONRef, so use can reuse arbitrary parts of your scenarios with `$ref` directive.
71
+ Properties are merged in a greedy way with type checking.
72
+
73
+ ### Multi-stage tests
74
+
75
+ Each test scenario contains 1+ stages; each stage is a single HTTP call.
76
+ `pytest-httpchain` executes stages in the order they are listed in scenario file; one stage failure stops the execution chain.
77
+
78
+ ### Common data context and variable substitution
79
+
80
+ `pytest-httpchain` maintains key-value data storage throughout the execution.
81
+ This storage ("common data context") is populated with declared variables, fixtures and data saved by stages. The data remains there throughout the scenario execution.
82
+ Writing scenarios, you can use Jinja-style expressions like `"{{ var }}"` for JSON values. `pytest-httpchain` does variable substitution dynamically right before executing a stage, and uses common data context keys as variables in these expressions.
83
+ Values from common data context also might be verified during verified/asserted.
84
+
85
+ ### User functions
86
+
87
+ `pytest-httpchain` can import and call regular python functions:
88
+
89
+ - to extract data from HTTP response
90
+ - to verify HTTP response and values in common data context
91
+ - to provide [custom authentication for requests](https://requests.readthedocs.io/en/latest/user/advanced/#custom-authentication)
92
+
93
+ ### JMESPath support
94
+
95
+ `pytest-httpchain` can extract values from JSON responses using JMESPath expressions directly.
96
+
97
+ ### JSON schema support
98
+
99
+ `pytest-httpchain` can verify JSON reponses against user-defined JSON schema.
100
+
101
+ ## Quick Start
102
+
103
+ Create a JSON test file named like `test_<name>.<suffix>.json` (default suffix is `http`):
104
+
105
+ ```python
106
+ # conftest.py
107
+ import pytest
108
+ from datetime import datetime
109
+
110
+ @pytest.fixture
111
+ def now_utc():
112
+ return datetime.now()
113
+ ```
114
+
115
+ ```json
116
+ {
117
+ "vars": {
118
+ "user_id": 1
119
+ },
120
+ "stages": [
121
+ {
122
+ "name": "get_user",
123
+ "request": {
124
+ "url": "https://api.example.com/users/{{ user_id }}"
125
+ },
126
+ "response": [
127
+ {
128
+ "verify": {
129
+ "status": 200
130
+ }
131
+ },
132
+ {
133
+ "save": {
134
+ "vars": {
135
+ "user_name": "user.name"
136
+ }
137
+ }
138
+ }
139
+ ]
140
+ },
141
+ {
142
+ "name": "update_user",
143
+ "fixtures": ["now_utc"],
144
+ "request": {
145
+ "url": "https://api.example.com/users/{{ user_id }}",
146
+ "method": "PUT",
147
+ "body": {
148
+ "json": {
149
+ "user": {
150
+ "name": "{{ user_name }}_updated",
151
+ "timestamp": "{{ str(now_utc) }}"
152
+ }
153
+ }
154
+ }
155
+ },
156
+ "response": [
157
+ {
158
+ "verify": {
159
+ "status": 200
160
+ }
161
+ }
162
+ ]
163
+ },
164
+ {
165
+ "name": "cleanup",
166
+ "always_run": true,
167
+ "request": {
168
+ "url": "https://api.example.com/cleanup",
169
+ "method": "POST"
170
+ }
171
+ }
172
+ ]
173
+ }
174
+ ```
175
+
176
+ Scenario we created:
177
+
178
+ - common data context is seeded with the first variable `user_id`
179
+ - **get_user**
180
+ url is assembled using `user_id` variable from common data context
181
+ HTTP GET call is made
182
+ we verify the call returned code 200
183
+ assuming JSON body is returned, we extract a value by JMESPath expression `user.name` and save it to common data context under `user_name` key
184
+ - **update_user**
185
+ `now_utc` fixture value is injected into common data context
186
+ url is assembled using `user_id` variable from common data context
187
+ we create JSON body in place using values from common data context, note that `now_utc` is converted to string in place
188
+ HTTP PUT call with body is made
189
+ we verify the call returned code 200
190
+ - **cleanup**
191
+ finalizing call meant for graceful exit
192
+ `always_run` parameter means this stage will be executed regardless of errors in previous stages
193
+
194
+ For detailed examples see [USAGE.md](USAGE.md).
195
+
196
+ ## Configuration
197
+
198
+ - Test file discovery is based on this name pattern: `test_<name>.<suffix>.json`.
199
+ The `suffix` is configurable as pytest ini option, default value is **http**.
200
+ - `$ref` instructions can point to other files; absolute and relative paths are supported.
201
+ You can limit the depth of relative path traversal using `ref_parent_traversal_depth` ini option, default value is **3**.
202
+
203
+ ## MCP Server
204
+
205
+ `pytest-httpchain` includes an MCP (Model Context Protocol) server to aid AI code assistants.
206
+
207
+ ### Installation
208
+
209
+ The optional dependency `mcp` installs MCP server's package and `pytest-httpchain-mcp` script.
210
+ Use this script as call target for your MCP configuration.
211
+
212
+ Claude Code `.mcp.json` example:
213
+
214
+ ```json
215
+ {
216
+ "mcpServers": {
217
+ "pytest-httpchain": {
218
+ "type": "stdio",
219
+ "command": "uv",
220
+ "args": ["run", "pytest-httpchain-mcp"],
221
+ "env": {}
222
+ }
223
+ }
224
+ }
225
+ ```
226
+
227
+ ### Features
228
+
229
+ The MCP server provides:
230
+
231
+ - **Scenario validation** - validate test scenario and scan for possible problems
232
+
233
+ ## Documentation
234
+
235
+ - [Usage Examples](USAGE.md) - Practical code examples
236
+ - [Full Documentation](https://aeresov.github.io/pytest-httpchain) - Complete guide
237
+ - [Changelog](CHANGELOG.md) - Release notes
238
+
239
+ ## Thanks
240
+
241
+ `pytest-httpchain` was heavily inspired by [Tavern](https://github.com/taverntesting/tavern) and [pytest-play](https://github.com/davidemoro/pytest-play).
242
+ [requests](https://requests.readthedocs.io) does the comms.
243
+ [Pydantic](https://docs.pydantic.dev) keeps the structure.
244
+ [pytest-order](https://github.com/pytest-dev/pytest-order) powers the chaining.
245
+ [pytest-datadir](https://github.com/gabrielcnr/pytest-datadir) saved me a lot of elbow grease.
@@ -0,0 +1,16 @@
1
+ pytest_httpchain/__init__.py,sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,0
2
+ pytest_httpchain/carrier.py,sha256=e696c8ea2e50e8c447816ea881bf4bb7cd4dc477e6f2b6f0d8e62dd107972b1b,5156
3
+ pytest_httpchain/carrier_factory.py,sha256=4d2f64a6c0cdf81ee4c826d30412f71f7ae742a77c9482e97d9971534dcd917d,3363
4
+ pytest_httpchain/constants.py,sha256=f7801ebd9317048468e1f4cc99858a3bd85dfafb6d475ab0f6bd344fa1814891,258
5
+ pytest_httpchain/context.py,sha256=b082e8026db698e61821e0884fa1a3629b3bc3535c8a5ed6bcf074318f805d20,3035
6
+ pytest_httpchain/exceptions.py,sha256=8cb12a71213596999b974064c0d89cc13c51fedfbf456ad5a6cbc8366f21772d,1209
7
+ pytest_httpchain/helpers.py,sha256=ed12adcedc1bafe3060d168007f2052cc208552a0b1615ab7c58bdbc6e3f1bab,1597
8
+ pytest_httpchain/plugin.py,sha256=522eaaa4a05f3b108c10c55867b28ee78167853f22ad9d4d092f75f880c0af1c,6701
9
+ pytest_httpchain/request.py,sha256=d81ea9f90e0fc0ad9d3304da74abb7902e6827d2646a8a7c5d44272dc2a565f0,3327
10
+ pytest_httpchain/response.py,sha256=9f023c48292fb7e87c57c00f37b5f782a23354a9f7c40a452397b9a746fa0007,6350
11
+ pytest_httpchain/stage_executor.py,sha256=fb2c887cd4c77cd06f8911e1e2aa5d6d7196cff08bd8d17eb28ffd7fb44dfcb0,4209
12
+ pytest_httpchain-0.1.1.dist-info/licenses/LICENSE,sha256=bcfbbc70256347518043fadc1e07484d958b8292c930f7745e6d083d583a35f5,1064
13
+ pytest_httpchain-0.1.1.dist-info/WHEEL,sha256=ab6157bc637547491fb4567cd7ddf26b04d63382916ca16c29a5c8e94c9c9ef7,79
14
+ pytest_httpchain-0.1.1.dist-info/entry_points.txt,sha256=7db8dc2819e062d7e780e1375205a8441e75757e09da2ace5eda7ae579f73616,55
15
+ pytest_httpchain-0.1.1.dist-info/METADATA,sha256=6032f330323a1f4abeee90df5b2fd53a1786332f4f7de8bd80136db8b9660871,8449
16
+ pytest_httpchain-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.7.22
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [pytest11]
2
+ pytest_httpchain = pytest_httpchain.plugin
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 aeresov
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.