Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

The Design of Everyday APIs

The Design of Everyday APIs

What makes a good API for a library? Or more importantly, what makes it bad? This talk discusses the principles of what goes into user-centered design, and how best to apply those principles when writing a Python library for fellow developers.

Lynn Root

June 02, 2022
Tweet

More Decks by Lynn Root

Other Decks in Technology

Transcript

  1. – Don Norman, The Design of Everyday Things Design is

    concerned with how things work, how they are controlled, and the nature of the interaction between people and technology.
  2. – Don Norman, The Design of Everyday Things Design is

    concerned with how things work, how they are controlled, and the nature of the interaction between people and technology.
  3. – Don Norman, The Design of Everyday Things Two of

    the most important characteristics of good design are discoverability and understanding.
  4. 5 Key Elements of Discoverability ► Affordances ► Signi fi

    ers ► Constraints ► Mappings ► Feedback
  5. 1 of 2 class Message: def __init__(self, id: str, data:

    str, published_at: str) -> None: self.id = id self.data = data self.published_at = published_at Library
  6. class Message: def __init__(self, id: str, data: str, published_at: str)

    -> None: class PubSubClient: def __init__(self, topic: str, subscription: str) -> None: def create_topic(self) -> None: def create_subscription(self) -> None: def add_message(self, msg: Message, timeout: int, retries: int) -> None: def get_message(self, timeout: int, retries: int) -> Message | None: def mark_message_done(self, msg_id: str, timeout: int, retries: int) -> None: def clear_message_queue(self) -> None: pass def close_client(self) -> None: pass Library 2 of 2
  7. 1 of 2 client = chaos_queue.PubSubClient(TOPIC, SUBSCRIPTION) try: client.create_topic() except

    chaos_queue.TopicExists: pass try: client.create_subscription() except chaos_queue.SubscriptionExists: pass User
  8. 2 of 2 for i in range(5): message = chaos_queue.Message(

    id=str(uuid.uuid4()), data=f"hello {i}", published_at=datetime.datetime.now().isoformat() ) try: client.add_message(message, 1, 0) except (chaos_queue.TimeoutError, queue.Full): pass while True: message = client.get_message(2, 2) if not message: break print(message) process_data(message.data) client.mark_message_done(message, 0, 0) client.close_client() User
  9. for i in range(5): message = chaos_queue.Message( id=str(uuid.uuid4()), data=f"hello {i}",

    published_at=datetime.datetime.now().isoformat() ) try: client.add_message(message, 1, 0) except (chaos_queue.TimeoutError, queue.Full): pass while True: message = client.get_message(2, 2) if not message: break print(message) process_data(message.data) client.mark_message_done(message, 0, 0) client.close_client() User 2 of 2
  10. for i in range(5): message = chaos_queue.Message( id=str(uuid.uuid4()), data=f"hello {i}",

    published_at=datetime.datetime.now().isoformat() ) try: client.add_message(message, 1, 0) except (chaos_queue.TimeoutError, queue.Full): pass while True: message = client.get_message(2, 2) if not message: break print(message) process_data(message.data) client.mark_message_done(message, 0, 0) client.close_client() User 2 of 2
  11. for i in range(5): message = chaos_queue.Message( id=str(uuid.uuid4()), data=f"hello {i}",

    published_at=datetime.datetime.now().isoformat() ) try: client.add_message(message, 1, 0) except (chaos_queue.TimeoutError, queue.Full): pass while True: message = client.get_message(2, 2) if not message: break print(message) process_data(message.data) client.mark_message_done(message, 0, 0) client.close_client() User 2 of 2
  12. before class PubSubClient: def __init__(self, topic: str, subscription: str) ->

    None: def create_topic(self) -> None: def create_subscription(self) -> None: def add_message(self, msg: Message, timeout: int, retries: int) -> None: def get_message(self, timeout: int, retries: int) -> Message | None: def mark_message_done(self, msg_id: str, timeout: int, retries: int) -> None: def clear_message_queue(self) -> None: pass def close_client(self) -> None: pass Library
  13. before class PubSubClient: def __init__(self, topic: str, subscription: str) ->

    None: def create_topic(self) -> None: def create_subscription(self) -> None: def add_message(self, msg: Message, timeout: int, retries: int) -> None: def get_message(self, timeout: int, retries: int) -> Message | None: def mark_message_done(self, msg_id: str, timeout: int, retries: int) -> None: def clear_message_queue(self) -> None: pass def close_client(self) -> None: pass Library
  14. after class PubSubClient: def __init__(self, topic: str, subscription: str) ->

    None: def create_topic(self) -> None: def create_subscription(self) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close_client(self) -> None: Library
  15. after class PubSubClient: def __init__(self, topic: str, subscription: str) ->

    None: def create_topic(self) -> None: def create_subscription(self) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close_client(self) -> None: Library
  16. before Library class PubSubClient: def __init__(self, topic: str, subscription: str)

    -> None: def create_topic(self) -> None: def create_subscription(self) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close_client(self) -> None:
  17. before Library class PubSubClient: def __init__(self, topic: str, subscription: str)

    -> None: def create_topic(self) -> None: def create_subscription(self) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close_client(self) -> None:
  18. after class PubClient: def __init__(self, topic: str) -> None: def

    create(self) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: Library
  19. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: before Library
  20. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: before Library
  21. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: after Library
  22. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: after Library
  23. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: before Library
  24. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, timeout: int, retries: int) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, timeout: int, retries: int) -> Message | None: def ack(self, msg_id: str, timeout: int, retries: int) -> None: def drain(self) -> None: def close(self) -> None: before Library
  25. after class PubClient: def __init__(self, topic: str) -> None: def

    create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, timeout: int = 30, retries: int = 0, ) -> None: def close(self) -> None: class SubClient: ... Library
  26. after class PubClient: def __init__(self, topic: str) -> None: def

    create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, timeout: int = 30, retries: int = 0, request_id: RequestIdType | bytes | str | None = None, debug: DebugLevelType | int | bool | None = None, ) -> None: def close(self) -> None: class SubClient: ... Library
  27. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, **kwargs) -> None: def close(self) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message | None: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  28. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, **kwargs) -> None: def close(self) -> None: before Library
  29. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, msg: Message, **kwargs) -> None: def close(self) -> None: before Library
  30. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: after Library
  31. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: after Library
  32. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: ... for message in msgs: self._queue.put(message, timeout=timeout) def close(self) -> None: after Library
  33. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message | None: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  34. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message | None: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  35. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message | None: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  36. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  37. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: ... try: return self._queue.get(timeout=timeout) except queue.Empty: raise ChaosEmptyError("Subscription queue is empty!") def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  38. class Message: def __init__(self, id: str, data: str, published_at: str)

    -> None: self.id = id self.data = data self.published_at = published_at before Library
  39. OptStr = str | None class Message: def __init__( self,

    data: str, id: OptStr = None, published_at: OptStr = None ): self.data = data self.id = id or str(uuid.uuid4()) self.published_at = published_at or datetime.datetime.now().isoformat() after Library
  40. OptStr = str | None class Message: def __init__( self,

    data: str, id: OptStr = None, published_at: OptStr = None ): self.data = data self.id = id or str(uuid.uuid4()) self.published_at = published_at or datetime.datetime.now().isoformat() def __repr__(self) -> str: return f"Message(id={self.id}, published_at={self.published_at})" after Library
  41. from dataclasses import dataclass, field @dataclass class Message: data: str

    = field(repr=False) id: str = field(default=None) published_at: str = field(default=None) def __post_init__(self): self.id = self.id or str(uuid.uuid4()) self.published_at = self.published_at or datetime.datetime.now().isoformat option 2 after Library
  42. from attrs import define, field @define class Message: data: str

    = field(repr=False) id: str = field() published_at: str = field() @id.default def set_id_default(self): return str(uuid.uuid4()) @published_at.default def set_published_at_default(self): return datetime.datetime.now().isoformat() after Library option 3
  43. Use domain nomenclature Clumsy naming hints at clumsy abstractions Provide

    symmetry Provide sane defaults Minimize repetition Be predictable and precise Let users be lazy Flexible Intuitive Tenet 3…
  44. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  45. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  46. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, msg_id: str, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  47. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, msg: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  48. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, msg: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  49. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, *msgs: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  50. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, *msgs: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  51. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def ack(self, *msgs: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: before Library
  52. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def iter(self, **kwargs) -> Iterator[Message]: ... while not self._queue.empty(): message = self._queue.get(timeout=timeout) yield message def ack(self, *msgs: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  53. class SubClient: def __init__(self, subscription: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def iter(self, **kwargs) -> Iterator[Message]: ... while not self._queue.empty(): message = self._queue.get(timeout=timeout) yield message def ack(self, *msgs: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: after Library
  54. before class PubClient: def __init__(self, topic: str) -> None: def

    create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: Library
  55. before class PubClient: def __init__(self, topic: str) -> None: def

    create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: Library
  56. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: def __enter__(self) -> Self: return self def __exit__(self, *args) -> None: self.close() after Library
  57. class PubClient: def __init__(self, topic: str) -> None: def create(self)

    -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: def __enter__(self) -> Self: return self def __exit__(self, *args) -> None: self.close() after Library
  58. ## Installation $ pip install chaos-queue ## Get Started import

    chaos_queue messages = [chaos_queue.Message(str(i)) for i in range(5)] pub_client = chaos_queue.PubClient(TOPIC) with pub_client.create(): pub_client.publish(*messages, timeout=1, retry=False) README.md
  59. ## Installation $ pip install chaos-queue ## Get Started import

    chaos_queue messages = [chaos_queue.Message(str(i)) for i in range(5)] pub_client = chaos_queue.PubClient(TOPIC) with pub_client.create(): pub_client.publish(*messages, timeout=1, retry=False) ## Learn more Just go to chaos-queue.readthedocs.io for documentation and more examples! README.md
  60. Use domain nomenclature Clumsy naming hints at clumsy abstractions Provide

    symmetry Provide sane defaults Minimize repetition Be predictable and precise Let users be lazy Provide composable functions Leverage language idioms Provide convenience Flexible Intuitive Simple
  61. before class Message: def __init__(self, id: str, data: str, published_at:

    str) -> None: self.id = id self.data = data self.published_at = published_at class PubSubClient: def __init__(self, topic: str, subscription: str) -> None: def create_topic(self) -> None: def create_subscription(self) -> None: def add_message(self, msg: Message, timeout: int, retries: int) -> None: def get_message(self, timeout: int, retries: int) -> Message | None: def mark_message_done(self, msg_id: str, timeout: int, retries: int) -> None: def clear_message_queue(self) -> None: pass def close_client(self) -> None: pass Library
  62. after @define class Message: data: str = field(repr=False) id: str

    = field() published_at: str = field() @id.default def set_id_default(self): return str(uuid.uuid4()) @published_at.default def set_published_at_default(self): return datetime.datetime.now().isoformat() class PubClient: def __init__(self, topic: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def publish(self, *msgs: Message, **kwargs) -> None: def close(self) -> None: def __enter__(self) -> Self: def __exit__(self, *args) -> None: class SubClient: def __init__(self, subscription: str) -> None: def create(self) -> None: def delete(self) -> None: def update(self, config: dict) -> None: def pull(self, **kwargs) -> Message: def iter(self, **kwargs) -> Iterable[Message]: def ack(self, *msgs: Message, **kwargs) -> None: def drain(self) -> None: def close(self) -> None: def __enter__(self) -> Self: def __exit__(self, *args) -> None: Library
  63. before client = chaos_queue.PubSubClient(TOPIC, SUBSCRIPTION) try: client.create_topic() except chaos_queue.TopicExists: pass

    try: client.create_subscription() except chaos_queue.SubscriptionExists: pass for i in range(5): message = chaos_queue.Message( id=str(uuid.uuid4()), data=f"hello {i}", published_at=datetime.datetime.now().isoformat() ) try: client.add_message(message, 1, False) except (chaos_queue.TimeoutError, queue.Full): pass while True: message = client.get_message(2, True) if not message: break print(message) process_data(message.data) client.mark_message_done(message, None, False) client.close_client() User
  64. after messages = [chaos_queue.Message(data=f"hello {i}") for i in range(5)] with

    chaos_queue.PubClient(TOPIC) as pub_client: pub_client.create() try: pub_client.publish(messages, timeout=1) except chaos_queue.TimeoutError: pass with chaos_queue.SubClient(SUBSCRIPTION) as sub_client: sub_client.create() for message in sub_client.iter(): print(message) process_data(message.data) sub_client.ack(message) User
  65. Use domain nomenclature Clumsy naming hints at clumsy abstractions Provide

    symmetry Provide sane defaults Minimize repetition Be predictable and precise Let users be lazy Provide composable functions Leverage language idioms Provide convenience Flexible Intuitive Simple