{"id":20697682,"url":"https://github.com/opennti/nti.webhooks","last_synced_at":"2025-09-02T13:13:03.934Z","repository":{"id":55950089,"uuid":"274443377","full_name":"OpenNTI/nti.webhooks","owner":"OpenNTI","description":"Python/Zope3 server-side webhooks implementation","archived":false,"fork":false,"pushed_at":"2023-04-24T18:06:54.000Z","size":306,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-08-19T06:26:06.700Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/OpenNTI.png","metadata":{"files":{"readme":"README.rst","changelog":"CHANGES.rst","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"docs/security.rst","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-06-23T15:34:19.000Z","updated_at":"2023-04-24T16:25:30.000Z","dependencies_parsed_at":"2024-11-17T00:33:29.239Z","dependency_job_id":null,"html_url":"https://github.com/OpenNTI/nti.webhooks","commit_stats":null,"previous_names":["nextthought/nti.webhooks"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/OpenNTI/nti.webhooks","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OpenNTI%2Fnti.webhooks","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OpenNTI%2Fnti.webhooks/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OpenNTI%2Fnti.webhooks/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OpenNTI%2Fnti.webhooks/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/OpenNTI","download_url":"https://codeload.github.com/OpenNTI/nti.webhooks/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/OpenNTI%2Fnti.webhooks/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273288234,"owners_count":25078687,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-02T02:00:09.530Z","response_time":77,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-17T00:19:25.031Z","updated_at":"2025-09-02T13:13:03.907Z","avatar_url":"https://github.com/OpenNTI.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"==============\n nti.webhooks\n==============\n\n.. image:: https://github.com/NextThought/nti.webhooks/workflows/tests/badge.svg\n   :target: https://github.com/NextThought/nti.webhooks/actions?query=workflow%3Atests\n\n.. image:: https://coveralls.io/repos/github/NextThought/nti.webhooks/badge.svg?branch=master\n   :target: https://coveralls.io/github/NextThought/nti.webhooks?branch=master\n\n.. image:: https://readthedocs.org/projects/ntiwebhooks/badge/?version=latest\n   :target: https://ntiwebhooks.readthedocs.io/en/latest/?badge=latest\n   :alt: Documentation Status\n\n\nThis package provides the infrastructure and delivery mechanisms for a\nserver to support webhook delivery. For complete details and the\nchangelog, see the `documentation\n\u003chttp://ntiwebhooks.readthedocs.io/\u003e`_.\n\n.. sphinx-include-begin-prelude\n\nWebhooks\n========\n\nWebhooks are HTTPS requests from one party --- the source --- to\nanother party, the destination. These requests are one-way: the source\nsends the request to the destination, and aside from conforming that\nthe request was received, takes no further action (the request's\nresponse from the destination is irrelevant). These requests are sent\nfrom the source to let the destination know that something has\nhappened: a new entity (or resource, in the REST sense) has been\ncreated, an old one updated or deleted, etc. Such requests typically\ncarry a payload in the body providing information about the action\n(usually the representation of the affected resource). Destinations\nare identified via complete URL; destinations may expect to be\ninformed of events affecting one, several, or all possible types of\nentities handled by the source.\n\nThis Package\n============\n\nThis package is installed in a source server and manages\nthe registration and sending of webhooks. The registrations may be\neither static, or they may be dynamic, as in the case of `REST Hooks\n\u003chttp://resthooks.org\u003e`_, where individual \"subscriptions\" may be\nstarted and stopped.\n\nThis package is intended to integrate with highly event-driven\napplications using `zope.event \u003chttps://zopeevent.readthedocs.io\u003e`_,\nthat define their resources using `zope.interface\n\u003chttps://zopeinterface.readthedocs.io\u003e`_, manage event delivery,\nresource adaptation, and dependency injection using `zope.component\n\u003chttps://zopecomponent.readthedocs.io\u003e`_, and (optionally) implement a\nhierarchy of component registries using `zope.site\n\u003chttps://zopesite.readthedocs.io\u003e`_ and `nti.site\n\u003chttps://ntisite.readthedocs.io\u003e`_. Data persistence is provided\nthrough `persistent objects \u003chttps://persistent.readthedocs.io\u003e`_,\ntypically with `ZODB \u003chttps://zodb-docs.readthedocs.io\u003e`_.\n\nData Model (Subscription Combinations)\n--------------------------------------\n\nOne of the motivating examples of this package is integration with\n`Zapier \u003chttps://zapier.com\u003e`_ and more generally the notion of `REST\nHooks \u003chttp://resthooks.org\u003e`_.\n\nIn this model, a configuration on a *server* (origin) that sends data\nto a *target* URL when events occur is called a *subscription.*\nSubscriptions are meant to include:\n\n#. An event name (or names) the subscription includes;\n#. A parent user or account relationship;\n#. A target URL; and\n#. Active vs inactive state.\n\nSubscription lookup must be performant, so the user and event name\ninformation for subscriptions should be fast to find.\n\nHere, event names are defined to \"use the noun.verb dot syntax, IE:\ncontact.create or lead.delete).\" Using ``zope.event`` and\n``zope.component,`` this translates to the pair of object type or\ninterface, and event type or interface. For example,\n``(IContentContainer, IObjectAddedEvent).``\n\nZapier generates a unique target URL for each event name, so to get\ncreated (added), modified, and deleted resources for a single type of\nobject there will be three different target URLs and thus three\ndifferent subscriptions. In general, there's an *N* x *M* expansion of\nobject types and event types to target URLs or subscriptions.\n\nThis package implements this model directly. (You can of course use\numbrella interfaces applied to multiple object or event types to send\nrelated events to a single subscription.) Aggregating data views of\n\"all webhook deliveries for a type of object\" or \"all webhook\ndeliveries for a type of event\" for presentation purposes could\nbe written, but isn't particularly natural given how its set up now.\n\nAn important outcome of this model is that there's no need for any\ngiven HTTP request to explicitly include something that identifies the\ntype of event; the default dialect (see below) assume that the URL\nincludes everything the receiver needs for that and doesn't do\nanything like add an X-NTI-EventType header or add something to the\nJSON body. It can be a URL parameter or a whole different URL, doesn't\nmatter.\n\n.. sphinx-include-after-prelude\n\nOut Of Scope\n============\n\nCertain concerns are out of scope for this package (but other packages\nbuilt upon this package my provide them). These include, but are not\nlimited to:\n\n- Providing a user interface for managing subscriptions.\n- Providing an HTTPS API for managing subscriptions. This package\n  provides the underlying data storage, but accepting parameters, etc,\n  and marshaling them into the correct Python calls, is not a concern\n  here.\n- Providing a user interface or HTTPS API for viewing webhook audit\n  logs.\n- Enabling webhooks to fire only for specific objects. This package\n  deals with scopes (sites) and kinds of objects, not individual instances.\n\nIn Scope/Features\n=================\n\nCertain concerns are very much in scope for this package, and this\npackage should provide a complete, easy to use solution that addresses\nthese concerns. Where necessary, if a concern cannot be addressed\ndirectly by this package, extension points (interfaces and\n``zope.component`` utilities) may be defined. These include, but are\nnot limited to:\n\n- Resource Representation\n\n  The on-the-wire form of the resources is built using\n  `nti.externalization \u003chttps://ntiexternalization.readthedocs.io\u003e`_.\n\n  To allow customization of the external forms, a named externalizer\n  is used; nti.externalization will fall back to the default\n  externalizer if no externalizer of the given name is available. The\n  default externalizer is named \"webhook-delivery\", but dialects may\n  use something different.\n\n- Alternate Webhook Dialects\n\n  Webhooks are a general protocol and mostly interoperable. But to\n  support cases where particular destinations have specific\n  requirements, \"dialects\" are used. There is a default dialect and\n  then there may be specializations of it. Each webhook subscription\n  may have associated with it the name of a dialect to use. These\n  dialects are found in the component registry. For example, a dialect\n  may choose to use a different externalizer name such as\n  \"zapier-webhook-delivery\".\n\n- Transactional\n\n  Webhooks should not be delivered if the ultimate creation or\n  persistence of a resource failed. To this end, webhook delivery in\n  this package is integrated with the `transaction\n  \u003chttps://transaction.readthedocs.io\u003e`_ package.\n\n  Resources are externalized during a late phase of the transaction\n  commit process; the details about the delivery are recorded and\n  persisted, and only after the transaction is successfully committed\n  does the HTTP request get made.\n\n- Concurrency\n\n  Webhook delivery and record keeping should be lightweight, and\n  all actual network IO should proceed in a non-blocking fashion. This\n  means that this package will spawn threads (or greenlets, using\n  `gevent \u003chttp://www.gevent.org\u003e`_.\n\n- Error Handling/Failure Retry\n\n  A limited amount of retry logic is provided by this package, but\n  that does not extend to process boundaries. If the process hosting\n  this package is killed while a delivery is pending, no automatic\n  provision is made to resume delivery attempts in any other process.\n\n  The API is present to allow that to be implemented, though.\n\n- Auditing/Delivery History\n\n  For each subscription, delivery attempts, status, and responses are\n  stored in a ring-buffer like structure. This can be inspected to see\n  if deliveries succeeded, failed, or never completed.\n\n- Access Control on Deliveries\n\n  Each subscription is associated with an ``IPrincipal`` that owns it.\n  A request is only delivered to a subscription if the ``IPrincipal``\n  that owns the subscription can access the entity, as determined by\n  `zope.security \u003chttps://zopesecurity.readthedocs.io\u003e`_.\n\n- Access Control on Subscriptions\n\n  While not enforced by this package, the above owner relationship\n  will be used to provide role managers that grant read and read/write\n  access to remove subscriptions only to the owner of the\n  subscription.\n\n  TODO: Make sure client packages can extend that to provide for admin\n  access. So long as we don't DENY it should be fine.\n\n- Hierarchy of Subscriptions\n\n  Subscriptions are made within a particular Zope site (the closest\n  enclosing site to a resource when a resource is subscribed to, or\n  the currently active site otherwise). These sites may have parents.\n\n  TODO: Work out the details of that.\n\n  When an event is received that might result in webhook delivery,\n  active subscriptions are checked for in the currently active site,\n  as well as in the sites up the hierarchy of the resource itself. All\n  applicable subscribers will get a delivery.\n\n  For example, if the president of the company (an administrator)\n  subscribes to \"new user created\" events at the global (root, base or\n  \"/\") level, and a department head subscribes to \"new user created\"\n  for their department (\"/NOAA\"), while a local office manager\n  subscribes to events for their office (\"/NOAA/NWS/OUN\"), then\n  creating a new user in the OKC office may send three deliveries, one\n  to the manager, one to the secretary, and one to the president.\n\n  .. note:: If there are identical subscribed URLs with differing permission\n            requirements, then if access is granted for *any\n            subscription*, the payload will be delivered.\n\n\n  .. note:: While looking up both the resource and active site tree\n            might seem complex, following both hierarchies is\n            necessary in the event of operations that span multiple\n            child sites. This is probably most common with bulk\n            operations, but a simple example would be the president\n            logging in to the root site, searching for and deleting\n            all employees named \"Bill.\" If one was in the OKC office\n            and one was in the OUN office, the managers of both\n            locations should get delivery.\n\n- Converting From Object Events to Webhook Events\n\n  TODO: Write me.\n\n  This package needs to have a clear way to have client packages\n  specify what events should produce webhook deliveries. The exact\n  mechanism is TBD. Possibly clients are expected to use\n  ``\u003cclassImplements\u003e`` ZCML directives to apply marker interfaces? Or\n  they might register a subscriber provided by this package for their\n  own existing interfaces?\n\n  We want this process, and the process of finding all active\n  subscriptions, to be fast. I'm imagining something like view lookup,\n  keeping active subscriptions in the various component registries?\n  That doesn't work non-persistently.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopennti%2Fnti.webhooks","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopennti%2Fnti.webhooks","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopennti%2Fnti.webhooks/lists"}