-
Notifications
You must be signed in to change notification settings - Fork 34
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
add KVStoreClient, examples #28
Conversation
.pre-commit-config.yaml
Outdated
@@ -27,6 +27,6 @@ repos: | |||
name: mypy | |||
stages: [commit] | |||
language: system | |||
entry: poetry run mypy **/*.py | |||
entry: poetry run mypy pykeybasebot/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was getting "No directory found" for **/*.py. Also I think this should match with what is in the Makefile for make test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could do poetry run mypy pykeybasebot examples tests
(though i think i was getting errors with the tests
dir)
examples/6_totp.py
Outdated
|
||
logging.basicConfig(level=logging.DEBUG) | ||
|
||
if "win32" in sys.platform: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ugh. we need to figure out how to pull this down into the framework. not for this PR. just saying it so it's not only in my head. these examples should have as little boilerplate as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
made this #29
examples/5_secret_storage.py
Outdated
cached = self.cache[entry_key].copy() if entry_key in self.cache else None | ||
if cached is not None: | ||
cached["info"] = ( | ||
cached["info"].copy() if cached["info"] is not None else None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we do a deepcopy of the self.cache[entry_key] instead?
#!/usr/bin/env python3 | ||
|
||
################################### | ||
# WHAT IS IN THIS EXAMPLE? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i really like the way this example came out :)
examples/5_secret_storage.py
Outdated
""" | ||
|
||
def __init__(self, *args, **kwargs): | ||
self.secret_kvstore_client = SecretKeyKVStoreClient(self) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is never referenced from the CachedBot (which is good, because it would be an implicit violation of the law of demeter). i'd suggest make this constraint explicit...
def __init__(self, *args, **kwargs):
secret_client = SecretKeyKVStoreClient(self)
self._cached_secret_kvstore = CachedKVStoreClient(self, secret_client)
super().__init__(*args, **kwargs)
@property
def cached_secret_kvstore(self):
return self._cached_secret_kvstore
if you're feeling bold, you could also make it like this...
@property
def kvstore(self):
return self._cached_secret_kvstore
which might simplify some of the other objects higher up in the stack: i.e. your RentalClient just needs the bot and it can interact with it like it would a normal bot
, this one has just been juiced up.
given the way this bot object has developed, i'd also consider renaming it from CachedBot
to something a little more telling. like maybe CustomBot
or JuicedUpBot
or PrivateConcurrentBot
? dunno. CachedBot
is a bit misleading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
after later comments, adding another suggestion...
class WhateverYouChooseBot(Bot):
@property
def kvstore(self):
if not self._cached_secret_kvstore
basic_client = KVStoreClient(self)
secret_client = SecretKeyKVStoreClient(basic_client)
self._cached_secret_kvstore = CachedKVStoreClient(secret_client)
return self._cached_secret_kvstore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok i renamed the bot class to CustomKVStoreBot, and this bot has a kvstore property that returns a CachedKVStoreClient.
then i renamed RentalClient to RentalBotClient. and i made RentalBotClient take in a Bot.
examples/5_secret_storage.py
Outdated
except RevisionError: | ||
# refresh cached value | ||
curr_info = await self.get(team, namespace, entry_key) | ||
return curr_info # failed put. return KVGetResult. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not sure i love this pattern. a PUT could return the PUT result OR a GET result?
not a blocker. i just think this is a little bit of a weird API.
self.secrets[team][namespace] = secret | ||
return self.secrets[team][namespace] | ||
|
||
async def hmac_key(self, team, namespace, entry_key) -> str: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NAB: it's probably a better practice to call this method something like hidden_key
. it's only incidental that it happens to do an hmac to achieve those ends.
return b64decode(x.encode("utf-8")) | ||
|
||
|
||
class SecretKeyKVStoreClient: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i really like the encapsulation of responsibility that this object now has. 👍
examples/5_secret_storage.py
Outdated
|
||
NAMESPACE = "rental" | ||
|
||
def __init__(self, kvstore: CachedKVStoreClient): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i don't know much about duck typing with python's type system, but this would be a case for it. it looks to me like this could take a CachedKVStoreClient or a SecretClient or a regular ol' client.
oh actually, what i suggested before was to pass in a Bot to this thing. yeah. in which case the ducktyping would be any object that has a kvstore
property.
this isn't important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahh. i see this is calling additional methods on the kvstore that don't exist on the basic one. ok. nevermind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok - i think i'd argue there's not really a compelling reason to expose get_cache
to the outside world. the only reason something outside of the CachedKVStoreClient would want to call it is for performance. and that's not really the point of this thing, it's for concurrency. plus it adds branching logic in this client that's just not really necessary. so yeah. i think i'd argue:
- the cached kvstore client shouldn't expose
get_cached
to the outside world - the rental client should always just call
get
(and now it never needs to worry about getting outdated data) - the rental client can be instantiated with anything kvstore-like. even if that's not a duck-typing exercise worth doing
examples/5_secret_storage.py
Outdated
expected_revision = 1 | ||
cached = self.kvstore.get_cached(tool) | ||
if cached is not None: | ||
if cached["info"]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah - it's sub-optimal that the RentalClient knows about how the CachedClient arranges its internal data structures
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looking great. just a couple small change requests. mostly renames and typos.
|
||
|
||
class CachedKVStoreClient: | ||
class TryingKVStoreClient: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oooo i like this name
examples/5_secret_storage.py
Outdated
res = await self.lookup(team, tool) | ||
info = ( | ||
json.loads(res.entry_value) if res.entry_value != "" else {} | ||
) # if tool already exists, propagate existing info |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
interesting choice. no opinions. :)
examples/5_secret_storage.py
Outdated
res = await self.kvstore.put( | ||
team, self.NAMESPACE, tool, info, expected_revision | ||
) | ||
return res |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NAB: maybe here we should return nil
because (1) you're good to go (and it's generally good practice to separate command from query methods), and (2) to distinguish it from an unsuccessful write
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or True/False for whether or not the reservation was accepted? (i.e. false if it's not accepted for an expected reason (i.e. someone else has that date))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
decided to change return type to Tuple[bool, Union[keybase1.KVGetResult, None]]
if (day not in info) or (day in info and info[day] != user): | ||
# failed to put because currently not reserved, or current reserver is not user | ||
return res | ||
expected_revision = res.revision + 1 | ||
res = await self.kvstore.put( | ||
team, self.NAMESPACE, tool, info, expected_revision | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
github won't let me comment on the line below, but same thing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great work! i think this is going to be really useful to people <3
can't release until keybase/client is released with kv store feature
@AMarcedone take a look at 4_secret_storage.py; i implemented the solution we discussed