mockstack 0.0.3__tar.gz

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.
Files changed (37) hide show
  1. mockstack-0.0.3/LICENSE +21 -0
  2. mockstack-0.0.3/PKG-INFO +13 -0
  3. mockstack-0.0.3/README.md +62 -0
  4. mockstack-0.0.3/mockstack/__init__.py +0 -0
  5. mockstack-0.0.3/mockstack/config.py +91 -0
  6. mockstack-0.0.3/mockstack/display.py +35 -0
  7. mockstack-0.0.3/mockstack/identifiers.py +84 -0
  8. mockstack-0.0.3/mockstack/lifespan.py +29 -0
  9. mockstack-0.0.3/mockstack/main.py +31 -0
  10. mockstack-0.0.3/mockstack/middleware.py +19 -0
  11. mockstack-0.0.3/mockstack/opentelemetry.py +45 -0
  12. mockstack-0.0.3/mockstack/routers/__init__.py +0 -0
  13. mockstack-0.0.3/mockstack/routers/catchall.py +33 -0
  14. mockstack-0.0.3/mockstack/routers/homepage.py +18 -0
  15. mockstack-0.0.3/mockstack/strategies/__init__.py +0 -0
  16. mockstack-0.0.3/mockstack/strategies/base.py +14 -0
  17. mockstack-0.0.3/mockstack/strategies/factory.py +21 -0
  18. mockstack-0.0.3/mockstack/strategies/filefixtures.py +230 -0
  19. mockstack-0.0.3/mockstack/templating.py +114 -0
  20. mockstack-0.0.3/mockstack/tests/__init__.py +0 -0
  21. mockstack-0.0.3/mockstack/tests/conftest.py +29 -0
  22. mockstack-0.0.3/mockstack/tests/fixtures/templates/__init__.py +0 -0
  23. mockstack-0.0.3/mockstack/tests/routers/__init__.py +0 -0
  24. mockstack-0.0.3/mockstack/tests/routers/test_catchall.py +46 -0
  25. mockstack-0.0.3/mockstack/tests/routers/test_homepage.py +27 -0
  26. mockstack-0.0.3/mockstack/tests/strategies/test_filefixtures.py +312 -0
  27. mockstack-0.0.3/mockstack/tests/test_display.py +33 -0
  28. mockstack-0.0.3/mockstack/tests/test_identifiers.py +94 -0
  29. mockstack-0.0.3/mockstack/tests/test_middleware.py +37 -0
  30. mockstack-0.0.3/mockstack/tests/test_templating.py +220 -0
  31. mockstack-0.0.3/mockstack.egg-info/PKG-INFO +13 -0
  32. mockstack-0.0.3/mockstack.egg-info/SOURCES.txt +35 -0
  33. mockstack-0.0.3/mockstack.egg-info/dependency_links.txt +1 -0
  34. mockstack-0.0.3/mockstack.egg-info/requires.txt +7 -0
  35. mockstack-0.0.3/mockstack.egg-info/top_level.txt +1 -0
  36. mockstack-0.0.3/pyproject.toml +30 -0
  37. mockstack-0.0.3/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [year] [fullname]
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.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: mockstack
3
+ Version: 0.0.3
4
+ Requires-Python: >=3.11
5
+ License-File: LICENSE
6
+ Requires-Dist: colorama>=0.4.6
7
+ Requires-Dist: fastapi[standard]>=0.115.12
8
+ Requires-Dist: jinja2>=3.1.6
9
+ Requires-Dist: opentelemetry-distro[otlp]>=0.53b1
10
+ Requires-Dist: opentelemetry-instrumentation-fastapi>=0.53b1
11
+ Requires-Dist: pydantic>=2.11.3
12
+ Requires-Dist: pydantic-settings>=2.9.1
13
+ Dynamic: license-file
@@ -0,0 +1,62 @@
1
+ # mockstack
2
+
3
+ [![CI](https://github.com/adamhadani/mockstack/actions/workflows/ci.yml/badge.svg)](https://github.com/adamhadani/mockstack/actions/workflows/ci.yml)
4
+ ![GitHub License](https://img.shields.io/github/license/adamhadani/mockstack)
5
+
6
+ An API mocking workhorse :racehorse:
7
+
8
+ Enabling a sane development lifecycle for microservice-oriented architectures.
9
+
10
+ Highlights include:
11
+
12
+ * Multiple strategies for handling requests such as Jinja2 template files with intelligent URL request-to-template routing, proxy strategy, and mixed strategies. :game_die:
13
+ * Observability via OpenTelemetry integration. Get detailed traces of your sessions instantly reported to backends such as Grafana, Jaeger, Zipkin, etc. :eyes:
14
+ * Configurability via `pydantic-settings` supports customizing behaviour via environment variables and a `.env` file. :flags:
15
+ * Comprehensive unit-tests, linting and formatting coverage as well as vulnerabilities and security scanning with full CI automation to ensure stability and a high-quality codebase for production-grade use. :+1:
16
+
17
+
18
+ ## Installation
19
+
20
+ Install using [uv](https://docs.astral.sh/uv/). This will create a virtualenv for you and install all dependencies:
21
+
22
+ uv sync
23
+ uv pip install -e .
24
+
25
+
26
+ ## Usage
27
+
28
+ Copy the included [.env.example](.env.example) file to `.env` and fill in configuration as needed based on the given examples.
29
+
30
+ Run in development mode (for live-reload of changes when developing):
31
+
32
+ uv run fastapi dev mockstack/main.py
33
+
34
+ Or, run in production mode:
35
+
36
+ uv run fastapi run mockstack/main.py
37
+
38
+ Available configuration options are [here](./mockstack/config.py). Setting individual options can be done with env. variables as in the following example:
39
+
40
+ MOCKSTACK__OPENTELEMETRY__ENABLED=true MOCKSTACK__OPENTELEMETRY__CAPTURE_RESPONSE_BODY=true uv run fastapi run mockstack/main.py
41
+
42
+ Out of the box, you get the following behavior when using the default `filefixtures` strategy:
43
+
44
+ - The HTTP request `GET /someservice/api/v1/user/c27f5b2b-6e81-420d-a4e4-6426e1c32db8` will try to find `<templates_dir>/someservice-api-v1-user.c27f5b2b-6e81-420d-a4e4-6426e1c32db8.j2`,
45
+ and will fallback to `<templates_dir>/someservice-api-v1-user.j2` (and finally to `index.j2` if exists). These are j2 files that have access to request body context variables.
46
+ - The HTTP request `POST /someservice/api/v2/item` with a JSON body will attempt to intelligently simulate the creation of a resource, returning the appropriate status code and will echo back the provided request resource, after injecting additional metadata fields based on strategy configuration. This is useful for services that expect fields such as `id` and `created_at` on returned created resources.
47
+ - HTTP requests for `DELETE` / `PUT` / `PATCH` are a no-op by default, simply returning the appropriate status code.
48
+ - The HTTP request `POST /someservice/api/v2/embedding_search` will be handled as a search request rather than a resource creation, returning an appropriate http status code and mock results based on user-configurable formatting.
49
+
50
+ Overall, the design philosophy is that things "just work". The framework attempts to intelligently deduce the intent of the request as much as possible and act accordingly,
51
+ while leaving room for advanced users to go in and customize behavior using the configuration options.
52
+
53
+
54
+ ## Testing
55
+
56
+ Invoke unit-tests with:
57
+
58
+ uv run python -m pytest
59
+
60
+ Linting, formatting, static type checks etc. are all managed via [pre-commit](https://pre-commit.com/) hooks. These will run automatically on every commit. You can invoke these manually on all files with:
61
+
62
+ pre-commit run --all-files
File without changes
@@ -0,0 +1,91 @@
1
+ from functools import lru_cache
2
+ from typing import Any, Literal
3
+
4
+ from pydantic import DirectoryPath
5
+ from pydantic_settings import BaseSettings, SettingsConfigDict
6
+
7
+
8
+ class OpenTelemetrySettings(BaseSettings):
9
+ """Settings for OpenTelemetry."""
10
+
11
+ enabled: bool = False
12
+
13
+ endpoint: str = "http://localhost:4317/"
14
+
15
+ # whether to capture the response body.
16
+ # this can be heavy, sensitive (PII) and/or not needed depending on the use case.
17
+ capture_response_body: bool = False
18
+
19
+
20
+ class Settings(BaseSettings):
21
+ """Settings for mockstack.
22
+
23
+ Default values are defined below and can be overwritten using an .env file
24
+ or with environment variables.
25
+
26
+ """
27
+
28
+ model_config = SettingsConfigDict(
29
+ env_prefix="mockstack__",
30
+ env_file=".env",
31
+ env_nested_delimiter="__",
32
+ )
33
+
34
+ # OpenTelemetry configuration
35
+ opentelemetry: OpenTelemetrySettings = OpenTelemetrySettings()
36
+
37
+ # strategy to use for handling requests
38
+ strategy: Literal["filefixtures", "proxyrules"] = "filefixtures"
39
+
40
+ # base directory for templates used by strategies
41
+ templates_dir: DirectoryPath = "./templates" # type: ignore[assignment]
42
+
43
+ # metadata fields to inject into created resources.
44
+ # A few template fields are available. See documentation for more details.
45
+ created_resource_metadata: dict[str, Any] = {
46
+ "id": "{{ uuid4() }}",
47
+ "createdAt": "{{ utcnow().isoformat() }}",
48
+ "updatedAt": "{{ utcnow().isoformat() }}",
49
+ "createdBy": "{{ request.headers.get('X-User-Id', uuid4()) }}",
50
+ "status": dict(code="OK", error_code=None),
51
+ }
52
+
53
+ # fields to inject into missing resources response json.
54
+ # some services may require such additional fields to be present in the response.
55
+ missing_resource_fields: dict[str, Any] = dict(
56
+ code=404,
57
+ message="mockstack: resource not found",
58
+ retryable=False,
59
+ )
60
+
61
+ # logging configuration. schema is based on the logging configuration schema:
62
+ # https://docs.python.org/3/library/logging.config.html#logging-config-dictschema
63
+ logging: dict[str, Any] = {
64
+ "version": 1,
65
+ "disable_existing_loggers": False,
66
+ "formatters": {
67
+ "standard": {
68
+ "format": " %(levelname)s [%(name)s] %(message)s",
69
+ },
70
+ },
71
+ "handlers": {
72
+ "console": {
73
+ "class": "logging.StreamHandler",
74
+ "level": "DEBUG",
75
+ "formatter": "standard",
76
+ "stream": "ext://sys.stdout",
77
+ },
78
+ },
79
+ "loggers": {
80
+ "FileFixturesStrategy": {
81
+ "handlers": ["console"],
82
+ "level": "INFO",
83
+ },
84
+ },
85
+ }
86
+
87
+
88
+ @lru_cache
89
+ def settings_provider() -> Settings:
90
+ """Provide the settings for the application."""
91
+ return Settings()
@@ -0,0 +1,35 @@
1
+ """Display and logging functionality."""
2
+
3
+ import logging
4
+
5
+ from mockstack.config import Settings
6
+
7
+
8
+ class ANSIColors:
9
+ HEADER = "\033[95m"
10
+ OKBLUE = "\033[94m"
11
+ OKCYAN = "\033[96m"
12
+ OKGREEN = "\033[92m"
13
+ WARNING = "\033[93m"
14
+ FAIL = "\033[91m"
15
+ ENDC = "\033[0m"
16
+ BOLD = "\033[1m"
17
+ UNDERLINE = "\033[4m"
18
+
19
+
20
+ def announce(settings: Settings):
21
+ """Log the startup message with the active settings."""
22
+ HIGHLIGHT = ANSIColors.HEADER
23
+ ENDC = ANSIColors.ENDC
24
+
25
+ logger = logging.getLogger("uvicorn")
26
+ logger.info(
27
+ f"{HIGHLIGHT}mockstack{ENDC} ready to roll. "
28
+ f"Using strategy: {HIGHLIGHT}{settings.strategy}{ENDC}, "
29
+ f"templates_dir: {HIGHLIGHT}{settings.templates_dir}{ENDC}. "
30
+ )
31
+ logger.info(
32
+ f"OpenTelemetry enabled: {HIGHLIGHT}{settings.opentelemetry.enabled}{ENDC}, "
33
+ f"endpoint: {HIGHLIGHT}{settings.opentelemetry.endpoint}{ENDC}, "
34
+ f"capture_response_body: {HIGHLIGHT}{settings.opentelemetry.capture_response_body}{ENDC}"
35
+ )
@@ -0,0 +1,84 @@
1
+ """Identifiers helpers."""
2
+
3
+ import itertools
4
+
5
+
6
+ def prefixes(iterable, reverse=False):
7
+ """Return an iterator of the prefixes of the iterable.
8
+
9
+ Examples:
10
+ ---------
11
+ >>> list(prefixes([1, 2, 3]))
12
+ [(1,), (1, 2), (1, 2, 3)]
13
+
14
+ >>> list(prefixes([1, 2, 3], reverse=True))
15
+ [(1, 2, 3), (1, 2), (1,)]
16
+
17
+ """
18
+ iterator = itertools.accumulate(map(lambda x: (x,), iterable))
19
+ if reverse:
20
+ return reversed(list(iterator))
21
+ return iterator
22
+
23
+
24
+ def looks_like_id(segment: str) -> bool:
25
+ """Check if a URL path segment looks like an ID.
26
+
27
+ Identifiers are typically numeric or hexadecimal (e.g. UUIDs) and are used to identify a resource.
28
+
29
+ We apply a few simple heuristics to try and provide a good balance between false positives and false negatives.
30
+
31
+ Examples:
32
+ ---------
33
+ >>> looks_like_id("123")
34
+ False # Odd length numeric
35
+
36
+ >>> looks_like_id("1234567890")
37
+ True # Even length numeric
38
+
39
+ >>> looks_like_id("1234567890abcdef")
40
+ True # Even length hex
41
+
42
+ >>> looks_like_id("3a4e5ad9-17ee-41af-972f-864dfccd4856")
43
+ True # UUID with dashes
44
+
45
+ >>> looks_like_id("3a4e5ad917ee41af972f864dfccd4856")
46
+ True # UUID without dashes
47
+
48
+ >>> looks_like_id("project")
49
+ False # Not a valid ID format
50
+
51
+ >>> looks_like_id("api")
52
+ False # Not a valid ID format
53
+
54
+ >>> looks_like_id("v1")
55
+ False # Not a valid ID format
56
+
57
+ """
58
+ if not segment or segment.isspace():
59
+ return False
60
+
61
+ # Check for special characters that aren't allowed in IDs
62
+ if any(c in segment for c in "_./+@"):
63
+ return False
64
+
65
+ N = len(segment)
66
+
67
+ # Check for UUID format (with or without dashes)
68
+ if N == 36:
69
+ # UUID with dashes
70
+ parts = segment.lower().split("-")
71
+ if len(parts) == 5 and all(
72
+ all(c in "0123456789abcdef" for c in p) for p in parts
73
+ ):
74
+ lengths = [len(p) for p in parts]
75
+ if lengths == [8, 4, 4, 4, 12]:
76
+ return True
77
+ elif N == 32:
78
+ # UUID without dashes
79
+ return all(c in "0123456789abcdefABCDEF" for c in segment)
80
+
81
+ # Check for even length numeric or hex
82
+ return (N % 2 == 0 and segment.isdigit()) or (
83
+ N % 2 == 0 and all(c in "0123456789abcdefABCDEF" for c in segment)
84
+ )
@@ -0,0 +1,29 @@
1
+ """FastAPI application lifecycle management."""
2
+
3
+ from contextlib import asynccontextmanager
4
+ from logging import config
5
+ from typing import Callable
6
+
7
+ from fastapi import FastAPI
8
+
9
+ from mockstack.config import Settings
10
+ from mockstack.display import announce
11
+
12
+
13
+ def lifespan_provider(
14
+ settings: Settings,
15
+ ) -> Callable:
16
+ """Provide the lifespan context manager."""
17
+
18
+ @asynccontextmanager
19
+ async def lifespan(app: FastAPI):
20
+ """FastAPI application lifespan management.
21
+
22
+ This is the context manager that FastAPI will use to manage the lifecycle of the application.
23
+ """
24
+ config.dictConfig(settings.logging)
25
+ announce(settings)
26
+
27
+ yield
28
+
29
+ return lifespan
@@ -0,0 +1,31 @@
1
+ """Application entrypoints."""
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from mockstack.config import settings_provider
6
+ from mockstack.lifespan import lifespan_provider
7
+ from mockstack.middleware import middleware_provider
8
+ from mockstack.opentelemetry import opentelemetry_provider
9
+ from mockstack.routers.catchall import catchall_router_provider
10
+ from mockstack.routers.homepage import homepage_router_provider
11
+ from mockstack.strategies.factory import strategy_provider
12
+
13
+
14
+ def create_app() -> FastAPI:
15
+ """Create the FastAPI app."""
16
+ settings = settings_provider()
17
+
18
+ app = FastAPI(lifespan=lifespan_provider(settings))
19
+
20
+ strategy_provider(app, settings)
21
+ middleware_provider(app, settings)
22
+ opentelemetry_provider(app, settings)
23
+
24
+ homepage_router_provider(app, settings)
25
+ catchall_router_provider(app, settings)
26
+
27
+ return app
28
+
29
+
30
+ # expose top-level app object for fastapi cli
31
+ app = create_app()
@@ -0,0 +1,19 @@
1
+ """Middleware definitionsfor the mockstack app."""
2
+
3
+ import time
4
+
5
+ from fastapi import FastAPI, Request
6
+
7
+ from mockstack.config import Settings
8
+
9
+
10
+ def middleware_provider(app: FastAPI, settings: Settings) -> None:
11
+ """Instrument the middlewares to the mockstack app."""
12
+
13
+ @app.middleware("http")
14
+ async def add_process_time_header(request: Request, call_next):
15
+ start_time = time.time()
16
+ response = await call_next(request)
17
+ process_time = time.time() - start_time
18
+ response.headers["X-Process-Time"] = str(process_time)
19
+ return response
@@ -0,0 +1,45 @@
1
+ """OpenTelemetry integration."""
2
+
3
+ from importlib import metadata
4
+
5
+ from fastapi import FastAPI, Request
6
+ from opentelemetry import trace
7
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
8
+ from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
9
+ from opentelemetry.sdk.resources import Resource
10
+ from opentelemetry.sdk.trace import TracerProvider
11
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
12
+
13
+ from mockstack.config import Settings
14
+
15
+
16
+ def span_name_for(request: Request) -> str:
17
+ """Get the span name for a request."""
18
+ return f"{request.method.upper()} {request.url.path}"
19
+
20
+
21
+ def opentelemetry_provider(app: FastAPI, settings: Settings) -> None:
22
+ """Initialize OpenTelemetry for the mockstack app."""
23
+ if not settings.opentelemetry.enabled:
24
+ return
25
+
26
+ # Initialize OpenTelemetry
27
+ distribution = metadata.distribution("mockstack")
28
+ resource = Resource(
29
+ attributes={
30
+ "service.name": distribution.name,
31
+ "service.version": distribution.version,
32
+ }
33
+ )
34
+
35
+ tracer_provider = TracerProvider(resource=resource)
36
+ trace.set_tracer_provider(tracer_provider)
37
+
38
+ # Set up OTLP exporter
39
+ otlp_exporter = OTLPSpanExporter(endpoint=settings.opentelemetry.endpoint)
40
+ span_processor = BatchSpanProcessor(otlp_exporter)
41
+ tracer_provider.add_span_processor(span_processor)
42
+
43
+ # Nb. we do not actually use the default FastAPIInstrumentor here
44
+ # because we use custom tracing in various places.
45
+ # FastAPIInstrumentor.instrument_app(app)
File without changes
@@ -0,0 +1,33 @@
1
+ """Routes for the mockstack app."""
2
+
3
+ from fastapi import FastAPI, Request
4
+ from opentelemetry import trace
5
+ from opentelemetry.propagate import extract
6
+
7
+ from mockstack.config import Settings
8
+ from mockstack.opentelemetry import span_name_for
9
+
10
+
11
+ def catchall_router_provider(app: FastAPI, settings: Settings) -> None:
12
+ """Create the catch-all routes for the mockstack app."""
13
+
14
+ @app.route("/{full_path:path}", methods=["GET", "PATCH", "POST", "PUT", "DELETE"])
15
+ async def catch_all(request: Request):
16
+ """Catch all requests and delegate to the strategy."""
17
+ tracer = trace.get_tracer(__name__)
18
+ ctx = extract(request.headers)
19
+ with tracer.start_as_current_span(span_name_for(request), context=ctx) as span:
20
+ span.set_attribute("http.method", request.method)
21
+ span.set_attribute("http.url", str(request.url))
22
+
23
+ response = await app.state.strategy.apply(request)
24
+
25
+ span.set_attribute("http.status_code", response.status_code)
26
+
27
+ # Nb. persisting response body can hamper performance,
28
+ # expose sensitive / PII data, and / or may not be needed.
29
+ # it is therefore an opt-in setting.
30
+ if settings.opentelemetry.capture_response_body:
31
+ span.set_attribute("response", response.body)
32
+
33
+ return response
@@ -0,0 +1,18 @@
1
+ """Routes for the homepage."""
2
+
3
+ from fastapi import APIRouter, FastAPI
4
+
5
+ from mockstack.config import Settings
6
+
7
+
8
+ def homepage_router_provider(app: FastAPI, settings: Settings) -> APIRouter:
9
+ """Provide the homepage routes."""
10
+
11
+ router = APIRouter()
12
+
13
+ @router.get("/")
14
+ async def homepage():
15
+ """Root endpoint."""
16
+ return {"Hello": "World"}
17
+
18
+ return router
File without changes
@@ -0,0 +1,14 @@
1
+ """Base strategy for MockStack."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from fastapi import Request, Response
6
+
7
+
8
+ class BaseStrategy(ABC):
9
+ """Base strategy for MockStack."""
10
+
11
+ @abstractmethod
12
+ async def apply(self, request: Request) -> Response:
13
+ """Apply the strategy to the request and response."""
14
+ pass
@@ -0,0 +1,21 @@
1
+ """Factory for creating strategies."""
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from mockstack.config import Settings
6
+ from mockstack.strategies.base import BaseStrategy
7
+ from mockstack.strategies.filefixtures import FileFixturesStrategy
8
+
9
+
10
+ def strategy_provider(app: FastAPI, settings: Settings) -> BaseStrategy:
11
+ """Factory for creating strategies."""
12
+
13
+ if settings.strategy == "filefixtures":
14
+ strategy = FileFixturesStrategy(settings)
15
+ else:
16
+ raise ValueError(f"Unknown strategy: {settings.strategy}")
17
+
18
+ # add strategy to app state for dependency injection
19
+ app.state.strategy = strategy
20
+
21
+ return strategy