Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix interaction between Task & Task2; add python.ExpressionView & task.RequestsPostData #33

Merged
merged 14 commits into from
Feb 26, 2019
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,29 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

<!--
## [Unreleased][]
-->

## [1.0.2] - 2019-02-21
### Added

- `transformer.python.ExpressionView`: An Expression that wraps a non-Expression
(e.g. a Request instance), similarly to how Standalone is a Statement that
wraps an Expression. A ExpressionView has a `target` (the wrapped object), a
`converter` (function capable of transforming the target into an Expression),
and a `name` for inspection purposes.

### Fixed

- A bug in the conversion between Task and Task2 makes Transformer ignore all
changes made by plugins to `Task2.request` in the generated locustfile.
Thank you [@xinke2411][] for reporting this! (#33)

### Removed

- `transformer.task.Task.as_locust_action`: As part of the merge between Task
and Task2 (#11). `as_locust_action` generates locustfile code as a string,
which is made obsolete by the `transformer.python` syntax tree framework. (#33)

## [1.0.2][] - 2019-02-21

### Added

Expand Down Expand Up @@ -38,14 +56,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `transformer.transform.transform` (#14)
- `transformer.locust.locustfile` (#14)

## [1.0.1] - 2019-02-12
## [1.0.1][] - 2019-02-12

### Fixed

- Fix `transformer` command-line crash due to a missing version identifier. (#17)
- Publish development releases to PyPI for every merge to the `master` branch. (#17)

## [1.0.0] - 2019-02-11
## [1.0.0][] - 2019-02-11

### Added

Expand All @@ -72,3 +90,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[1.0.2]: https://github.com/zalando-incubator/transformer/compare/v1.0.1...v1.0.2
[1.0.1]: https://github.com/zalando-incubator/transformer/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/zalando-incubator/transformer/compare/f842c4163e037dc345eaf1992187f58126b7d909...v1.0.0

[@xinke2411]: https://github.com/xinke2411
bmaher marked this conversation as resolved.
Show resolved Hide resolved
11 changes: 10 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pendulum = "^2.0"
chevron = "^0.13"
docopt = "^0.6.2"
ecological = "^1.6"
dataclasses = "^0.6.0"

[tool.poetry.dev-dependencies]
locustio = "^0.9.0"
Expand Down
19 changes: 6 additions & 13 deletions transformer/plugins/sanitize_headers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from transformer.helpers import zip_kv_pairs
from transformer.plugins import plugin, Contract
from transformer.request import Header
from transformer.task import Task2


Expand All @@ -10,17 +10,10 @@ def plugin(task: Task2) -> Task2:
Converts header names to lowercase to simplify further overriding.
Removes the cookie header as it is handled by Locust's HttpSession.
"""
headers = task.request.headers

if not isinstance(headers, dict):
headers = zip_kv_pairs(headers)

sanitized_headers = {
k.lower(): v
for (k, v) in headers.items()
if not k.startswith(":") and k.lower() != "cookie"
}

task.request = task.request._replace(headers=sanitized_headers)
task.request.headers = [
Header(name=h.name.lower(), value=h.value)
for h in task.request.headers
if not h.name.startswith(":") and h.name.lower() != "cookie"
]

return task
3 changes: 2 additions & 1 deletion transformer/plugins/test_sanitize_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ def test_it_removes_headers_beginning_with_a_colon():
def test_it_downcases_header_names():
task = task_with_header("Some Name", "some value")
sanitized_headers = plugin(task).request.headers
assert "some name" in sanitized_headers
header_names = {h.name for h in sanitized_headers}
assert "some name" in header_names


def test_it_removes_cookies():
Expand Down
44 changes: 44 additions & 0 deletions transformer/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
Tuple,
cast,
Iterable,
Callable,
TypeVar,
)

from dataclasses import dataclass

IMMUTABLE_EMPTY_DICT = MappingProxyType({})


Expand Down Expand Up @@ -686,3 +690,43 @@ def __repr__(self) -> str:
self.alias,
self.comments,
)


_T = TypeVar("_T")


@dataclass
class ExpressionView(Expression):
"""
The promise of an Expression representing an object currently not in
Expression format.

The ExpressionView allows to mix non-Expression objects in the syntax tree,
along with a function capable of transforming these objects into actual
Expression objects at any time.
This is useful when there is a simpler representation than Expression.

For instance, any Request object can be converted into an equivalent
Expression, but Request has a simpler API than Expression for all
request-oriented operations like accessing the URL, etc.
tortila marked this conversation as resolved.
Show resolved Hide resolved
Embedding a Request in a ExpressionView allows to pretend that the Request is
already in Expression format (with all associated benefits) but still use
the Request API.

`target` is a callable returning the non-Expression object. That callable
allows to specify as target some mutable field of an object, rather than a
fixed reference to an object. See for example task.Task2, which contains a
ExpressionView to its own "request" field; if the value of that field is
changed, the ExpressionView will refer to the new value instead of keeping a
reference to the old value.

`name` is purely descriptive: it can make inspection of data structures
containing ExpressionView objects more comfortable.
"""

name: str
target: Callable[[], _T]
converter: Callable[[_T], Expression]

def __str__(self) -> str:
return str(self.converter(self.target()))
26 changes: 19 additions & 7 deletions transformer/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@

import enum
from datetime import datetime
from typing import Iterator, NamedTuple, List
from typing import Iterator, NamedTuple, List, Optional
from urllib.parse import urlparse, SplitResult

import pendulum
from dataclasses import dataclass

from transformer.naming import to_identifier

Expand All @@ -34,7 +35,8 @@ class Header(NamedTuple):
value: str


class QueryPair(NamedTuple):
@dataclass
class QueryPair:
"""
Query String as recorded in HAR file.
"""
Expand All @@ -43,17 +45,27 @@ class QueryPair(NamedTuple):
value: str


class Request(NamedTuple):
@dataclass
class Request:
"""
An HTTP request as recorded in a HAR file.

Note that *post_data*, if present, will be a dict of the same format as read
in the HAR file.
Although not consistently followed by HAR generators, his format is
thilp marked this conversation as resolved.
Show resolved Hide resolved
documented here: http://www.softwareishard.com/blog/har-12-spec/#postData.
"""

timestamp: datetime
method: HttpMethod
url: SplitResult
headers: List[Header]
post_data: dict
query: List[QueryPair]
headers: List[Header] = ()
post_data: Optional[dict] = None
query: List[QueryPair] = ()

def __post_init__(self):
self.headers = list(self.headers)
self.query = list(self.query)

@classmethod
def from_har_entry(cls, entry: dict) -> "Request":
Expand Down Expand Up @@ -107,6 +119,6 @@ def __hash__(self) -> int:
self.timestamp,
self.method,
self.url,
tuple(self.post_data) if self.post_data else None,
tuple(self.post_data.items()) if self.post_data else None,
)
)
1 change: 0 additions & 1 deletion transformer/scenario.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
NamedTuple,
)


import transformer.plugins as plug
from transformer.naming import to_identifier
from transformer.plugins.contracts import Plugin
Expand Down
Loading