Type annotating for ndb.tasklets - python

GvRs App Engine ndb Library as well as monocle and - to my understanding - modern Javascript use Generators to make async code look like blocking code.
Things are decorated with #ndb.tasklet. They yield when they want to give back execution to the runloop and when they have their result ready they raise StopIteration(value) (or the alias ndb.Return):
#ndb.tasklet
def get_google_async():
context = ndb.get_context()
result = yield context.urlfetch("http://www.google.com/")
if result.status_code == 200:
raise ndb.Return(result.content)
raise RuntimeError
To use such a Function you get a ndb.Future object back and call the get get_result() Function on that to wait for the result and get it. E.g.:
def get_google():
future = get_google_async()
# do something else in real code here
return future.get_result()
This all works very nice. but how to add type Annotations? The correct types are:
get_google_async() -> ndb.Future (via yield)
ndb.tasklet(get_google_async) -> ndb.Future
ndb.tasklet(get_google_async).get_result() -> str
So far, I came only up with casting the async function.
def get_google():
# type: () -> str
future = get_google_async()
# do something else in real code here
return cast('str', future.get_result())
Unfortunately this is not only about urlfetch but about hundreds of Methods- mainly of ndb.Model.

get_google_async itself is a generator function, so type hints can be () -> Generator[ndb.Future, None, None], I think.
As for get_google, if you don't want to cast, type checking may work.
like
def get_google():
# type: () -> Optional[str]
future = get_google_async()
# do something else in real code here
res = future.get_result()
if isinstance(res, str):
return res
# somehow convert res to str, or
return None

Related

Playwright page.wait_for_event function how to access the page and other variables from inside the callable?

I'm trying to use the playwright page.wait_for_event function. One of the kwargs accepts a Callable. I'm trying to use a helper function that would take two arguments: the event that I'm waiting to fire, and a global variable. But I can't seem to figure out how to find and/or use a variable for the event to pass into the helper function. Most examples I see that use the wait_for_event function use a lambda function for the predicate argument which works great, but I need to perform an action before I return a boolean value which I also don't know how to do with a lambda function.
My apologies if my terminology is incorrect.
The function I'm trying to use as an argument:
def test(event, global_variable):
page.locator('//*[#id="n-currentevents"]/a').click() # Action before boolean
if event.url == 'https://en.wikipedia.org/':
return True
The variations of the page.wait_for_event function I tried:
# Doesn't work
r = page.wait_for_event('request', test('request', global_variable))
r = page.wait_for_event('request', test(event, global_variable))
r = page.wait_for_event(event='request', test(event, global_variable))
r = page.wait_for_event(event:'request', test(event, global_variable))
# Lambda works, but I need to click an element before returning the truth value
r = page.wait_for_event(event='request', lambda req : req.url ==
'https://en.wikipedia.org/')
The wait_for_event function:
def wait_for_event(
self, event: str, predicate: typing.Callable = None, *, timeout: float = None
) -> typing.Any:
"""Page.wait_for_event
> NOTE: In most cases, you should use `page.expect_event()`.
Waits for given `event` to fire. If predicate is provided, it passes event's value into the `predicate` function and
waits for `predicate(event)` to return a truthy value. Will throw an error if the page is closed before the `event` is
fired.
Parameters
----------
event : str
Event name, same one typically passed into `*.on(event)`.
predicate : Union[Callable, NoneType]
Receives the event data and resolves to truthy value when the waiting should resolve.
timeout : Union[float, NoneType]
Maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default
value can be changed by using the `browser_context.set_default_timeout()`.
Returns
-------
Any
"""
return mapping.from_maybe_impl(
self._sync(
self._impl_obj.wait_for_event(
event=event,
predicate=self._wrap_handler(predicate),
timeout=timeout,
)
)
)
Update: I was able to accomplish my task with a more appropriate method.
URL = "https://en.wikipedia.org"
with page.expect_response(lambda response: response.url == URL as response_info:
page.locator('//xpath').click()
response = response_info.value
You can use a factory function here to pass the page, and the global_variable. Keep in mind that navigating away from the page from inside the callable will lead to an error. So make sure whatever you are clicking does not change the current URL of the page.
def wrapper(page, global_variable):
def test(event):
page.locator('//*[#id="n-currentevents"]/a').click() # Action before boolean
if event.url == 'https://en.wikipedia.org/':
return True
return test
Then, you can register the above function using page.wait_for_event like this:
page.wait_for_event('request', wrapper(page, global_variable))
Remember: You need to pass functions/callables (not their return values) to page.wait_for_event

Conditional type hint

Im using a pattern that all my adapters returns a Result objects instead of the result itself. Let me explain:
from typing import Generic, Optional, TypeVar
from pydantic import BaseModel
Dto = TypeVar("Dto", bound=BaseModel)
class Result(BaseModel, Generic[Dto]):
error: Optional[Exception]
data: Optional[Dto]
#property
def is_success(self) -> bool:
return bool(self.data) and not self.error
class Config:
arbitrary_types_allowed = True
def adapter_example(input: Any) -> Result[int]:
try:
# some complex stuff here
result = Result[int](data=10)
except SomethingBad as e:
return Result[int](error=e)
The point is, check if error is None do not ensures me that data != None. There is a way to force that at least (and conditionally) one of them are mandatory (or, not Optional)?
Like:
Result[str](data='a') # VALID
Result[str](error=Exception()) # VALID
Result[str](data='', error=Exception()) # VALID
Result[str]() # INVALID
if result.data:
# Here any linter are 100% sure that result.error is None
else:
# Here any linter are 100% sure that result.error != None
ps: Im only using pydantic.BaseModel here because its easier in this implementation. If any sugestions about how to type this class conditionally dont use pydantic is fine to mee

How to throw types from Depends for mypy to Fastapi?

I have this exxxampe code.
def get_test(q: str) -> str:
return 'str str str' + q
#router_user.put(
"/", response_model=schemas.UpdateUserRequest, tags=["User and Authentication"]
)
async def update_user(
data: schemas.UpdateUserRequest,
q: str = Depends(logic.get_test),
db: AsyncSession = Depends(get_db),
user: models.User = Depends(logic.get_curr_user_by_token),
):
print(q)
when checking it with mypy, I always get an error
app/api/user/router.py:74: error: Incompatible default for argument "q" (default has type "Depends", argument has type "str") [assignment]
why??? and how do I fix it?
types are the worst thing that could be dragged into python.
Looking at your code it looks like your get_curr_user_by_token returns models.User. Depends itself doesn't change the type of result, so you just use the one that is returned by thing inside Depends.
q: models.User = Depends(logic.get_curr_user_by_token),

How to pass multiple parameters to Azure Durable Activity Function

My orchestrator receives a payload, with that payload it contains instructions that need to be passed along with other sets of data to activity functions.
how do I pass multiple parameters to an activity function? Or do I have to mash all my data together?
def orchestrator_function(context: df.DurableOrchestrationContext):
# User defined configuration
instructions: str = context.get_input()
task_batch = yield context.call_activity("get_tasks", None)
# Need to pass in instructions too
parallel_tasks = [context.call_activity("perform_task", task) for task in task_batch]
results = yield context.task_all(parallel_tasks)
return results
The perform_task activity needs both the items from task_batch and the user input instructions
Do I do something in my function.json?
Workaround
Not ideal, but I can pass multiple parameters as a single Tuple
something = yield context.call_activity("activity", ("param_1", "param_2"))
I then just need to reference the correct index of the parameter in the activity.
Seems there's no text-book way to do it. I have opted to give my single parameter a generic name like parameter or payload.
Then when passing in the value in the orchestrator I do it like so:
payload = {"value_1": some_var, "value_2": another_var}
something = yield context.call_activity("activity", payload)
then within the activity function, I unpack it again.
edit: Some buried documentation seems to show that https://learn.microsoft.com/en-us/azure/azure-functions/durable/durable-functions-error-handling?tabs=python
Just to add to #Ari's great answer, here code to pass data from client function (HTTP request in this case) all the way to activity function:
Client -> Orchestrator -> Activity
Client
async def main(req: func.HttpRequest, starter: str) -> func.HttpResponse:
client = df.DurableOrchestrationClient(starter)
req_data = req.get_json()
img_url = req_data['img_url']
payload = {"img_url": img_url}
instance_id = await client.start_new(req.route_params["functionName"], None, payload)
logging.info(f"Started orchestration with ID = '{instance_id}'.")
return client.create_check_status_response(req, instance_id)
Orchestrator
def orchestrator_function(context: df.DurableOrchestrationContext):
input_context = context.get_input()
img_url = input_context.get('img_url')
some_response= yield context.call_activity('MyActivity', img_url)
return [some_response]
Activity
def main(imgUrl: str) -> str:
print(f'.... Image URL = {imgUrl}')
return imgUrl
You can use #dataclass and #dataclass_json class decorators for your input and output types, like this:
#dataclass_json
#dataclass
class Command:
param1: str
param2: int
#dataclass_json
#dataclass
class Result:
val1: str
val2: int
and then you can use those in Azure Functions, e.g. in Activity ones:
def main(input: DownloadFileRequest) -> DownloadFileResponse:
# function code
result: DownloadFileResponse = DownloadFileResponse("some", 123)
return result
This provides you with a clean API and descriptive code. Much better approach than using dictionaries, at least for me.
I would also suggest the dataclass-wizard as a viable option, and one which should also be a bit more lightweight alternative to dataclasses-json; it is lighter in the sense that it does not use external libraries like marshmallow for generating schemas. It also performs a little better FWIW anyway.
I didn't really understand the schema of the data as outlined in the question unfortunately, thus I decided to roll my own for the purposes of a quick and dirty demo, so check it out:
from dataclasses import dataclass
from dataclass_wizard import JSONWizard
#dataclass
class Batman(JSONWizard):
suit_color: str
side: 'Sidekick'
#dataclass
class Sidekick:
name: str
mood: str
# usage
bats = Batman.from_dict({'suitColor': 'BLACK',
'side': {'Name': 'Robin', 'mood': 'Mildly depressed'}})
print(repr(bats))
# prints:
# Batman(suit_color='BLACK', side=Sidekick(name='Robin', mood='Mildly depressed'))
print(bats.to_dict())
# prints:
# {'suitColor': 'BLACK', 'side': {'name': 'Robin', 'mood': 'Mildly depressed'}}

Elegant pattern for mutually exclusive keyword args?

Sometimes in my code I have a function which can take an argument in one of two ways. Something like:
def func(objname=None, objtype=None):
if objname is not None and objtype is not None:
raise ValueError("only 1 of the ways at a time")
if objname is not None:
obj = getObjByName(objname)
elif objtype is not None:
obj = getObjByType(objtype)
else:
raise ValueError("not given any of the ways")
doStuffWithObj(obj)
Is there any more elegant way to do this? What if the arg could come in one of three ways? If the types are distinct I could do:
def func(objnameOrType):
if type(objnameOrType) is str:
getObjByName(objnameOrType)
elif type(objnameOrType) is type:
getObjByType(objnameOrType)
else:
raise ValueError("unk arg type: %s" % type(objnameOrType))
But what if they are not? This alternative seems silly:
def func(objnameOrType, isName=True):
if isName:
getObjByName(objnameOrType)
else:
getObjByType(objnameOrType)
cause then you have to call it like func(mytype, isName=False) which is weird.
How about using something like a command dispatch pattern:
def funct(objnameOrType):
dispatcher = {str: getObjByName,
type1: getObjByType1,
type2: getObjByType2}
t = type(objnameOrType)
obj = dispatcher[t](objnameOrType)
doStuffWithObj(obj)
where type1,type2, etc are actual python types (e.g. int, float, etc).
Sounds like it should go to https://codereview.stackexchange.com/
Anyway, keeping the same interface, I may try
arg_parsers = {
'objname': getObjByName,
'objtype': getObjByType,
...
}
def func(**kwargs):
assert len(kwargs) == 1 # replace this with your favorite exception
(argtypename, argval) = next(kwargs.items())
obj = arg_parsers[argtypename](argval)
doStuffWithObj(obj)
or simply create 2 functions?
def funcByName(name): ...
def funcByType(type_): ...
One way to make it slightly shorter is
def func(objname=None, objtype=None):
if [objname, objtype].count(None) != 1:
raise TypeError("Exactly 1 of the ways must be used.")
if objname is not None:
obj = getObjByName(objname)
else:
obj = getObjByType(objtype)
I have not yet decided if I would call this "elegant".
Note that you should raise a TypeError if the wrong number of arguments was given, not a ValueError.
For whatever it's worth, similar kinds of things happen in the Standard Libraries; see, for example, the beginning of GzipFile in gzip.py (shown here with docstrings removed):
class GzipFile:
myfileobj = None
max_read_chunk = 10 * 1024 * 1024 # 10Mb
def __init__(self, filename=None, mode=None,
compresslevel=9, fileobj=None):
if mode and 'b' not in mode:
mode += 'b'
if fileobj is None:
fileobj = self.myfileobj = __builtin__.open(filename, mode or 'rb')
if filename is None:
if hasattr(fileobj, 'name'): filename = fileobj.name
else: filename = ''
if mode is None:
if hasattr(fileobj, 'mode'): mode = fileobj.mode
else: mode = 'rb'
Of course this accepts both filename and fileobj keywords and defines a particular behavior in the case that it receives both; but the general approach seems pretty much identical.
I use a decorator:
from functools import wraps
def one_of(kwarg_names):
# assert that one and only one of the given kwarg names are passed to the decorated function
def inner(f):
#wraps(f)
def wrapped(*args, **kwargs):
count = 0
for kw in kwargs:
if kw in kwarg_names and kwargs[kw] is not None:
count += 1
assert count == 1, f'exactly one of {kwarg_names} required, got {kwargs}'
return f(*args, **kwargs)
return wrapped
return inner
Used as:
#one_of(['kwarg1', 'kwarg2'])
def my_func(kwarg1='default', kwarg2='default'):
pass
Note that this only accounts for non- None values that are passed as keyword arguments. E.g. multiple of the kwarg_names may still be passed if all but one of them have a value of None.
To allow for passing none of the kwargs simply assert that the count is <= 1.
It sounds like you're looking for function overloading, which isn't implemented in Python 2. In Python 2, your solution is nearly as good as you can expect to get.
You could probably bypass the extra argument problem by allowing your function to process multiple objects and return a generator:
import types
all_types = set([getattr(types, t) for t in dir(types) if t.endswith('Type')])
def func(*args):
for arg in args:
if arg in all_types:
yield getObjByType(arg)
else:
yield getObjByName(arg)
Test:
>>> getObjByName = lambda a: {'Name': a}
>>> getObjByType = lambda a: {'Type': a}
>>> list(func('IntType'))
[{'Name': 'IntType'}]
>>> list(func(types.IntType))
[{'Type': <type 'int'>}]
The built-in sum() can be used to on a list of boolean expressions. In Python, bool is a subclass of int, and in arithmetic operations, True behaves as 1, and False behaves as 0.
This means that this rather short code will test mutual exclusivity for any number of arguments:
def do_something(a=None, b=None, c=None):
if sum([a is not None, b is not None, c is not None]) != 1:
raise TypeError("specify exactly one of 'a', 'b', or 'c'")
Variations are also possible:
def do_something(a=None, b=None, c=None):
if sum([a is not None, b is not None, c is not None]) > 1:
raise TypeError("specify at most one of 'a', 'b', or 'c'")
I occasionally run into this problem as well, and it is hard to find an easily generalisable solution. Say I have more complex combinations of arguments that are delineated by a set of mutually exclusive arguments and want to support additional arguments for each (some of which may be required and some optional), as in the following signatures:
def func(mutex1: str, arg1: bool): ...
def func(mutex2: str): ...
def func(mutex3: int, arg1: Optional[bool] = None): ...
I would use object orientation to wrap the arguments in a set of descriptors (with names depending on the business meaning of the arguments), which can then be validated by something like pydantic:
from typing import Optional
from pydantic import BaseModel, Extra
# Extra.forbid ensures validation error if superfluous arguments are provided
class BaseDescription(BaseModel, extra=Extra.forbid):
pass # Arguments common to all descriptions go here
class Description1(BaseDescription):
mutex1: str
arg1: bool
class Description2(BaseDescription):
mutex2: str
class Description3(BaseDescription):
mutex3: int
arg1: Optional[bool]
You could instantiate these descriptions with a factory:
class DescriptionFactory:
_class_map = {
'mutex1': Description1,
'mutex2': Description2,
'mutex3': Description3
}
#classmethod
def from_kwargs(cls, **kwargs) -> BaseDescription:
kwargs = {k: v for k, v in kwargs.items() if v is not None}
set_fields = kwargs.keys() & cls._class_map.keys()
try:
[set_field] = set_fields
except ValueError:
raise ValueError(f"exactly one of {list(cls._class_map.keys())} must be provided")
return cls._class_map[set_field](**kwargs)
#classmethod
def validate_kwargs(cls, func):
def wrapped(**kwargs):
return func(cls.from_kwargs(**kwargs))
return wrapped
Then you can wrap your actual function implementation like this and use type checking to see which arguments were provided:
#DescriptionFactory.validate_kwargs
def func(desc: BaseDescription):
if isinstance(desc, Description1):
... # use desc.mutex1 and desc.arg1
elif isinstance(desc, Description2):
... # use desc.mutex2
... # etc.
and call as func(mutex1='', arg1=True), func(mutex2=''), func(mutex3=123) and so on.
This is not overall shorter code, but it performs argument validation in a very descriptive way according to your specification, raises useful pydantic errors when validation fails, and results in accurate static types in each branch of the function implementation.
Note that if you're using Python 3.10+, structural pattern matching could simplify some parts of this.

Categories