tuxedo


Nametuxedo JSON
Version 1.0.9 PyPI version JSON
download
home_pagehttps://github.com/aivarsk/tuxedo-python
SummaryPython3 bindings for writing Oracle Tuxedo clients and servers
upload_time2022-12-20 20:40:40
maintainer
docs_urlNone
authorAivars Kalvans
requires_python
licenseMIT
keywords
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            =============
Tuxedo-Python
=============

.. image:: https://github.com/aivarsk/tuxedo-python/workflows/CI/badge.svg
    :target: https://github.com/aivarsk/tuxedo-python

Python3 bindings for writing Oracle Tuxedo clients and servers. With Python2 support. With a book Modernizing Oracle Tuxedo Applications with Python:

.. image:: https://ws-na.amazon-adsystem.com/widgets/q?_encoding=UTF8&MarketPlace=US&ASIN=180107058X&ServiceVersion=20070822&ID=AsinImage&WS=1&Format=_SL160_&tag=aivarsk-20
    :target: https://amzn.to/3ljktiH

Why?
----

I'm a fan of the way `tuxmodule <https://github.com/henschkowski/tuxmodule/blob/master/README.md>`_ enables you to interact with Oracle Tuxedo. Unfortunately, it's out-dated and somehow limited. So I cloned tuxmodule and started to clean up compiler warnings and work on some features I had in mind:

- A multi-threaded server
- Support nested FML32 buffers and more types
- Support newest Oracle Tuxedo features like ``tpadvertisex()`` and ``tpappthrinit()``
- Receive response even when the service returns TPFAIL (instead of exception)

But I realized that's too much of C for me, so I decided to write my own Python module for Oracle Tuxedo in C++ and `pybind11 <https://github.com/pybind/pybind11>`_ focusing on the parts I find most important first.

Supported platforms
-------------------

The code is tested on the latest versions of Ubuntu and Oracle Linux using GCC 8 compiler. It should work on other Linux distributions and other UNIXes with no or minor adjustments.

Windows runtime requirements (experimental)
-------------------------------------------

On Windows, the Visual C++ redistributable packages are a runtime requirement for this project. It can be found `here <https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads>`_.

I have successfully built the module with Python 3.7.7.

Python 3.8.3 fails to import the module `similar to <https://github.com/psycopg/psycopg2/issues/1006>`_ but I have no solution yet.

.. code:: Python

  ImportError: DLL load failed while importing tuxedo: The specified module could not be found.

Alternatives to Oracle Tuxedo
-----------------------------

Tuxedo-Python can also be used with `Open Source alternative to Oracle Tuxedo called Fuxedo <https://github.com/fuxedo/fuxedo>`_. Just export ``TUXDIR`` pointing to the folder where `Fuxedo <http://fuxedo.io>`_ is installed everything should work.

All demo code provided with the module works with both Oracle Tuxedo and Fuxedo and you can avoid vendor lock-in by using Python and Tuxedo-Python module.

General
-------

``tuxedo`` module supports only ``STRING``, ``CARRAY`` and ``FML32`` buffer types at the moment.

``CARRAY`` is mapped to/from Python ``bytes`` type.

``STRING`` is mapped to/from Python ``str`` type.

``FML32`` is mapped to/from Python ``dict`` type with field names (``str``) as keys and lists (``list``) of different types (``int``, ``str``, ``float`` or ``dict``) as values. ``dict`` to ``FML32`` conversion also treats types ``int``, ``str``, ``float`` or ``dict`` as lists with a single element:

.. code:: python

  {'TA_CLASS': 'Single value'}

converted to ``FML32`` and then back to ``dict`` becomes

.. code:: python

  {'TA_CLASS': ['Single value']}


All XATMI functions that take buffer and length arguments in C take only buffer argument in Python.

Calling a service
-----------------

``tuxedo.tpcall()`` and ``tuxedo.tpgetrply()`` functions return a tuple with 3 elements or throw an exception when no data is received. This is the part I believe ``tuxmodule`` got wrong: a service may return a response
both when it succeeds (``TPSUCCESS``) and fails (``TPFAIL``) and often the failure response contains some important information.

- 0 or ``TPESVCFAIL``
- ``tpurcode`` (the second argument to ``tpreturn``)
- data buffer

.. code:: python

  rval, rcode, data = t.tpcall('.TMIB', {'TA_CLASS': 'T_SVCGRP', 'TA_OPERATION': 'GET'})
  if rval == 0:
    # Service returned TPSUCCESS
  else:
    # rval == tuxedo.TPESVCFAIL
    # Service returned TPFAIL 

Writing servers
---------------

Tuxedo servers are written as Python classes. ``tpsvrinit`` method of object will be called when Tuxedo calls ``tpsvrinit(3c)`` function and it must return 0 on success or -1 on error. A common task for ``tpsvrinit`` is to advertise services the server provides by calling ``tuxedo.tpadvertise()`` with a service name. A method with the same name must exist. ``tpsvrdone``, ``tpsvrthrinit`` and ``tpsvrthrdone`` will be called when Tuxedo calls corresponding functions. All of these 4 methods are optional and ``tuxedo`` module always calls ``tpopen()`` and ``tpclose()`` functions before calling user-supplied methods.

Each service method receives a single argument with incoming buffer and service must end with either call to ``tuxedo.tpreturn()`` or ``tuxedo.tpforward()``. Unlike in C ``tuxedo.tpreturn()`` and ``tuxedo.tpforward()`` do not perform ``longjmp`` but set up arguments for those calls once service method will return. You can have a code that will execute after Python's ``tpreturn`` and it plays nicely with context managers. Following two code fragments are equivalent but I believe the first one is less error-prone.

.. code:: python

      def ECHO(self, args):
          return t.tpreturn(t.TPSUCCESS, 0, args)

.. code:: python

      def ECHO(self, args):
          t.tpreturn(t.TPSUCCESS, 0, args)


After that ``tuxedo.run()`` must be called with an instance of the class and command-line arguments to start Tuxedo server's main loop.

.. code:: python

  #!/usr/bin/env python3
  import sys
  import tuxedo as t

  class Server:
      def tpsvrinit(self, args):
          t.tpadvertise('ECHO')
          return 0

      def tpsvrthrinit(self, args):
          return 0

      def tpsvrthrdone(self):
          pass

      def tpsvrdone(self):
          pass

      def ECHO(self, args):
          return t.tpreturn(t.TPSUCCESS, 0, args)

  if __name__ == '__main__':
      t.run(Server(), sys.argv)

UBBCONFIG
---------

To use Python code as Tuxedo server the file itself must be executable (``chmod +x *.py``) and it must contain shebang line with Python:

.. code:: python

  #!/usr/bin/env python3

After that you can use the ``*.py`` file as server executable in ``UBBCONFIG``:

.. code::

  "api.py" SRVGRP=GROUP1 SRVID=20 RQADDR="api" MIN=1 SECONDARYRQ=Y REPLYQ=Y

Writing clients
---------------

Nothing special is needed to implement Tuxedo clients, just import the module and start calling XATMI functions.

.. code:: python

  #!/usr/bin/env python3
  import sys
  import tuxedo as t

  rval, rcode, data = t.tpcall('.TMIB', {'TA_CLASS': 'T_SVCGRP', 'TA_OPERATION': 'GET'})

Using Oracle Database
---------------------

You can access Oracle database with ``cx_Oracle`` library and local transactions by just following the documentation of ``cx_Oracle``.

If you want a server written in Python to participate in the global transaction first specify a resource manager name to use (similar to ``buidserver``). ``tuxedo`` module currently supports:

- NONE default "null" resource manager
- Oracle_XA for Oracle Database

.. code:: python

    t.run(Server(), sys.argv, 'Oracle_XA')


After that you should create a database connection in ``tpsvrinit`` by using ``tuxedo.xaoSvcCtx()`` function:

.. code:: python

    def tpsvrinit(self, args):
        self.db = cx_Oracle.connect(handle=t.xaoSvcCtx())

That is the only difference from standard ``cx_Oracle`` use case. Here is a complete example for a single-threaded server:

.. code:: python

  #!/usr/bin/env python3

  import sys
  import tuxedo as t
  import cx_Oracle

  class Server:
      def tpsvrinit(self, args):
          t.userlog('Server startup')
          self.db = cx_Oracle.connect(handle=t.xaoSvcCtx())
          t.tpadvertise('DB')
          return 0

      def DB(self, args):
          dbc = self.db.cursor()
          dbc.execute('insert into pymsg(msg) values (:1)', ['Hello from python'])
          return t.tpreturn(t.TPSUCCESS, 0, args)

  if __name__ == '__main__':
      t.run(Server(), sys.argv, 'Oracle_XA')

For a multi-threaded server new connections for each thread must be created in ``tpsvrthrinit()`` (instead of ``tpsvrinit()``) and stored in thread-local storage of ``threading.local()``.

Server must belong to a group with ``Oracle_XA`` as resource manager, something like this in ``UBBCONFIG``

.. code::

  *GROUPS
  GROUP2 LMID=tuxapp GRPNO=2 TMSNAME=ORACLETMS OPENINFO="Oracle_XA:Oracle_XA+Objects=true+Acc=P/scott/tiger+SqlNet=ORCL+SesTm=60+LogDir=/tmp+Threads=true"
  *SERVERS
  "db.py" SRVGRP=GROUP2 SRVID=2 CLOPT="-A"


tpadmcall
---------

``tpadmcall`` is made available for application administration even while application is down. It also has no service call overhead compared to calling ``.TMIB`` service. The Python function looks and behaves similary to ``tpcall`` except ``rcode`` (2nd element in result tuple) is always a constant 0.

.. code:: python

  #!/usr/bin/env python3
  import tuxedo as t

  rval, _, data = t.tpadmcall({'TA_CLASS': 'T_DOMAIN', 'TA_OPERATION': 'GET'})


Global transactions
-------------------

Transactions can be started and committed or aborted by using ``tuxedo.tpbegin()``, ``tuxedo.tpcommit()``, ``tuxedo.tpabort()``. These functions take the same arguments as their corresponding C functions.

Buffer export and import
------------------------



FML32 identifiers
-----------------

``Fname32`` and ``Fldid32`` are available to find map from field identifier to name or the other way.

Functions to determine field number and type from identifier:

.. code:: python

  assert t.Fldtype32(t.Fmkfldid32(t.FLD_STRING, 10)) == t.FLD_STRING
  assert t.Fldno32(t.Fmkfldid32(t.FLD_STRING, 10)) == 10

Exceptions
----------

On errors either ``XatmiException`` or ``Fml32Exception`` are raised by the module. Exceptions contain additional attirbute ``code`` that contains the Tuxedo error code and you can compare it with defined errors like ``TPENOENT`` or ``TPESYSTEM``.

.. code:: python

  try:
    t.tpcall("whatever", {})
  except t.XatmiException as e:
    if e.code == t.TPENOENT:
      print("Service does not exist")


Demo
----

``demo/`` folder has some proof-of-concept code:

- ``client.py`` Oracle Tuxedo client
- ``api.py`` HTTP+JSON server running inside Oracle Tuxedo server
- ``ecb.py`` HTTP+XML client running inside Oracle Tuxedo server
- ``mem.py`` multi-threaded in-memory cache
- ``db.py`` Access Oracle Database using cx_Oracle module within global transaction
- ``buf.py`` Demo of tpimport/tpexport and FML32 identifiers

TODO
----

- Implementing few more useful APIs
            

Raw data

            {
    "_id": null,
    "home_page": "https://github.com/aivarsk/tuxedo-python",
    "name": "tuxedo",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "",
    "keywords": "",
    "author": "Aivars Kalvans",
    "author_email": "aivars.kalvans@gmail.com",
    "download_url": "https://files.pythonhosted.org/packages/e6/84/8af193a980e0d4937ebf1a132befefc1671e8cc2cc48f01911e736d33574/tuxedo-1.0.9.tar.gz",
    "platform": null,
    "description": "=============\nTuxedo-Python\n=============\n\n.. image:: https://github.com/aivarsk/tuxedo-python/workflows/CI/badge.svg\n    :target: https://github.com/aivarsk/tuxedo-python\n\nPython3 bindings for writing Oracle Tuxedo clients and servers. With Python2 support. With a book Modernizing Oracle Tuxedo Applications with Python:\n\n.. image:: https://ws-na.amazon-adsystem.com/widgets/q?_encoding=UTF8&MarketPlace=US&ASIN=180107058X&ServiceVersion=20070822&ID=AsinImage&WS=1&Format=_SL160_&tag=aivarsk-20\n    :target: https://amzn.to/3ljktiH\n\nWhy?\n----\n\nI'm a fan of the way `tuxmodule <https://github.com/henschkowski/tuxmodule/blob/master/README.md>`_ enables you to interact with Oracle Tuxedo. Unfortunately, it's out-dated and somehow limited. So I cloned tuxmodule and started to clean up compiler warnings and work on some features I had in mind:\n\n- A multi-threaded server\n- Support nested FML32 buffers and more types\n- Support newest Oracle Tuxedo features like ``tpadvertisex()`` and ``tpappthrinit()``\n- Receive response even when the service returns TPFAIL (instead of exception)\n\nBut I realized that's too much of C for me, so I decided to write my own Python module for Oracle Tuxedo in C++ and `pybind11 <https://github.com/pybind/pybind11>`_ focusing on the parts I find most important first.\n\nSupported platforms\n-------------------\n\nThe code is tested on the latest versions of Ubuntu and Oracle Linux using GCC 8 compiler. It should work on other Linux distributions and other UNIXes with no or minor adjustments.\n\nWindows runtime requirements (experimental)\n-------------------------------------------\n\nOn Windows, the Visual C++ redistributable packages are a runtime requirement for this project. It can be found `here <https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads>`_.\n\nI have successfully built the module with Python 3.7.7.\n\nPython 3.8.3 fails to import the module `similar to <https://github.com/psycopg/psycopg2/issues/1006>`_ but I have no solution yet.\n\n.. code:: Python\n\n  ImportError: DLL load failed while importing tuxedo: The specified module could not be found.\n\nAlternatives to Oracle Tuxedo\n-----------------------------\n\nTuxedo-Python can also be used with `Open Source alternative to Oracle Tuxedo called Fuxedo <https://github.com/fuxedo/fuxedo>`_. Just export ``TUXDIR`` pointing to the folder where `Fuxedo <http://fuxedo.io>`_ is installed everything should work.\n\nAll demo code provided with the module works with both Oracle Tuxedo and Fuxedo and you can avoid vendor lock-in by using Python and Tuxedo-Python module.\n\nGeneral\n-------\n\n``tuxedo`` module supports only ``STRING``, ``CARRAY`` and ``FML32`` buffer types at the moment.\n\n``CARRAY`` is mapped to/from Python ``bytes`` type.\n\n``STRING`` is mapped to/from Python ``str`` type.\n\n``FML32`` is mapped to/from Python ``dict`` type with field names (``str``) as keys and lists (``list``) of different types (``int``, ``str``, ``float`` or ``dict``) as values. ``dict`` to ``FML32`` conversion also treats types ``int``, ``str``, ``float`` or ``dict`` as lists with a single element:\n\n.. code:: python\n\n  {'TA_CLASS': 'Single value'}\n\nconverted to ``FML32`` and then back to ``dict`` becomes\n\n.. code:: python\n\n  {'TA_CLASS': ['Single value']}\n\n\nAll XATMI functions that take buffer and length arguments in C take only buffer argument in Python.\n\nCalling a service\n-----------------\n\n``tuxedo.tpcall()`` and ``tuxedo.tpgetrply()`` functions return a tuple with 3 elements or throw an exception when no data is received. This is the part I believe ``tuxmodule`` got wrong: a service may return a response\nboth when it succeeds (``TPSUCCESS``) and fails (``TPFAIL``) and often the failure response contains some important information.\n\n- 0 or ``TPESVCFAIL``\n- ``tpurcode`` (the second argument to ``tpreturn``)\n- data buffer\n\n.. code:: python\n\n  rval, rcode, data = t.tpcall('.TMIB', {'TA_CLASS': 'T_SVCGRP', 'TA_OPERATION': 'GET'})\n  if rval == 0:\n    # Service returned TPSUCCESS\n  else:\n    # rval == tuxedo.TPESVCFAIL\n    # Service returned TPFAIL \n\nWriting servers\n---------------\n\nTuxedo servers are written as Python classes. ``tpsvrinit`` method of object will be called when Tuxedo calls ``tpsvrinit(3c)`` function and it must return 0 on success or -1 on error. A common task for ``tpsvrinit`` is to advertise services the server provides by calling ``tuxedo.tpadvertise()`` with a service name. A method with the same name must exist. ``tpsvrdone``, ``tpsvrthrinit`` and ``tpsvrthrdone`` will be called when Tuxedo calls corresponding functions. All of these 4 methods are optional and ``tuxedo`` module always calls ``tpopen()`` and ``tpclose()`` functions before calling user-supplied methods.\n\nEach service method receives a single argument with incoming buffer and service must end with either call to ``tuxedo.tpreturn()`` or ``tuxedo.tpforward()``. Unlike in C ``tuxedo.tpreturn()`` and ``tuxedo.tpforward()`` do not perform ``longjmp`` but set up arguments for those calls once service method will return. You can have a code that will execute after Python's ``tpreturn`` and it plays nicely with context managers. Following two code fragments are equivalent but I believe the first one is less error-prone.\n\n.. code:: python\n\n      def ECHO(self, args):\n          return t.tpreturn(t.TPSUCCESS, 0, args)\n\n.. code:: python\n\n      def ECHO(self, args):\n          t.tpreturn(t.TPSUCCESS, 0, args)\n\n\nAfter that ``tuxedo.run()`` must be called with an instance of the class and command-line arguments to start Tuxedo server's main loop.\n\n.. code:: python\n\n  #!/usr/bin/env python3\n  import sys\n  import tuxedo as t\n\n  class Server:\n      def tpsvrinit(self, args):\n          t.tpadvertise('ECHO')\n          return 0\n\n      def tpsvrthrinit(self, args):\n          return 0\n\n      def tpsvrthrdone(self):\n          pass\n\n      def tpsvrdone(self):\n          pass\n\n      def ECHO(self, args):\n          return t.tpreturn(t.TPSUCCESS, 0, args)\n\n  if __name__ == '__main__':\n      t.run(Server(), sys.argv)\n\nUBBCONFIG\n---------\n\nTo use Python code as Tuxedo server the file itself must be executable (``chmod +x *.py``) and it must contain shebang line with Python:\n\n.. code:: python\n\n  #!/usr/bin/env python3\n\nAfter that you can use the ``*.py`` file as server executable in ``UBBCONFIG``:\n\n.. code::\n\n  \"api.py\" SRVGRP=GROUP1 SRVID=20 RQADDR=\"api\" MIN=1 SECONDARYRQ=Y REPLYQ=Y\n\nWriting clients\n---------------\n\nNothing special is needed to implement Tuxedo clients, just import the module and start calling XATMI functions.\n\n.. code:: python\n\n  #!/usr/bin/env python3\n  import sys\n  import tuxedo as t\n\n  rval, rcode, data = t.tpcall('.TMIB', {'TA_CLASS': 'T_SVCGRP', 'TA_OPERATION': 'GET'})\n\nUsing Oracle Database\n---------------------\n\nYou can access Oracle database with ``cx_Oracle`` library and local transactions by just following the documentation of ``cx_Oracle``.\n\nIf you want a server written in Python to participate in the global transaction first specify a resource manager name to use (similar to ``buidserver``). ``tuxedo`` module currently supports:\n\n- NONE default \"null\" resource manager\n- Oracle_XA for Oracle Database\n\n.. code:: python\n\n    t.run(Server(), sys.argv, 'Oracle_XA')\n\n\nAfter that you should create a database connection in ``tpsvrinit`` by using ``tuxedo.xaoSvcCtx()`` function:\n\n.. code:: python\n\n    def tpsvrinit(self, args):\n        self.db = cx_Oracle.connect(handle=t.xaoSvcCtx())\n\nThat is the only difference from standard ``cx_Oracle`` use case. Here is a complete example for a single-threaded server:\n\n.. code:: python\n\n  #!/usr/bin/env python3\n\n  import sys\n  import tuxedo as t\n  import cx_Oracle\n\n  class Server:\n      def tpsvrinit(self, args):\n          t.userlog('Server startup')\n          self.db = cx_Oracle.connect(handle=t.xaoSvcCtx())\n          t.tpadvertise('DB')\n          return 0\n\n      def DB(self, args):\n          dbc = self.db.cursor()\n          dbc.execute('insert into pymsg(msg) values (:1)', ['Hello from python'])\n          return t.tpreturn(t.TPSUCCESS, 0, args)\n\n  if __name__ == '__main__':\n      t.run(Server(), sys.argv, 'Oracle_XA')\n\nFor a multi-threaded server new connections for each thread must be created in ``tpsvrthrinit()`` (instead of ``tpsvrinit()``) and stored in thread-local storage of ``threading.local()``.\n\nServer must belong to a group with ``Oracle_XA`` as resource manager, something like this in ``UBBCONFIG``\n\n.. code::\n\n  *GROUPS\n  GROUP2 LMID=tuxapp GRPNO=2 TMSNAME=ORACLETMS OPENINFO=\"Oracle_XA:Oracle_XA+Objects=true+Acc=P/scott/tiger+SqlNet=ORCL+SesTm=60+LogDir=/tmp+Threads=true\"\n  *SERVERS\n  \"db.py\" SRVGRP=GROUP2 SRVID=2 CLOPT=\"-A\"\n\n\ntpadmcall\n---------\n\n``tpadmcall`` is made available for application administration even while application is down. It also has no service call overhead compared to calling ``.TMIB`` service. The Python function looks and behaves similary to ``tpcall`` except ``rcode`` (2nd element in result tuple) is always a constant 0.\n\n.. code:: python\n\n  #!/usr/bin/env python3\n  import tuxedo as t\n\n  rval, _, data = t.tpadmcall({'TA_CLASS': 'T_DOMAIN', 'TA_OPERATION': 'GET'})\n\n\nGlobal transactions\n-------------------\n\nTransactions can be started and committed or aborted by using ``tuxedo.tpbegin()``, ``tuxedo.tpcommit()``, ``tuxedo.tpabort()``. These functions take the same arguments as their corresponding C functions.\n\nBuffer export and import\n------------------------\n\n\n\nFML32 identifiers\n-----------------\n\n``Fname32`` and ``Fldid32`` are available to find map from field identifier to name or the other way.\n\nFunctions to determine field number and type from identifier:\n\n.. code:: python\n\n  assert t.Fldtype32(t.Fmkfldid32(t.FLD_STRING, 10)) == t.FLD_STRING\n  assert t.Fldno32(t.Fmkfldid32(t.FLD_STRING, 10)) == 10\n\nExceptions\n----------\n\nOn errors either ``XatmiException`` or ``Fml32Exception`` are raised by the module. Exceptions contain additional attirbute ``code`` that contains the Tuxedo error code and you can compare it with defined errors like ``TPENOENT`` or ``TPESYSTEM``.\n\n.. code:: python\n\n  try:\n    t.tpcall(\"whatever\", {})\n  except t.XatmiException as e:\n    if e.code == t.TPENOENT:\n      print(\"Service does not exist\")\n\n\nDemo\n----\n\n``demo/`` folder has some proof-of-concept code:\n\n- ``client.py`` Oracle Tuxedo client\n- ``api.py`` HTTP+JSON server running inside Oracle Tuxedo server\n- ``ecb.py`` HTTP+XML client running inside Oracle Tuxedo server\n- ``mem.py`` multi-threaded in-memory cache\n- ``db.py`` Access Oracle Database using cx_Oracle module within global transaction\n- ``buf.py`` Demo of tpimport/tpexport and FML32 identifiers\n\nTODO\n----\n\n- Implementing few more useful APIs",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "Python3 bindings for writing Oracle Tuxedo clients and servers",
    "version": "1.0.9",
    "split_keywords": [],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "md5": "a688c2e87c1b3106e0f3c8253196f3a4",
                "sha256": "79b5071a8077a5b861162af9c6d1e5a244b54085a951250f9f4456399d43f57e"
            },
            "downloads": -1,
            "filename": "tuxedo-1.0.9.tar.gz",
            "has_sig": false,
            "md5_digest": "a688c2e87c1b3106e0f3c8253196f3a4",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 22625,
            "upload_time": "2022-12-20T20:40:40",
            "upload_time_iso_8601": "2022-12-20T20:40:40.830589Z",
            "url": "https://files.pythonhosted.org/packages/e6/84/8af193a980e0d4937ebf1a132befefc1671e8cc2cc48f01911e736d33574/tuxedo-1.0.9.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2022-12-20 20:40:40",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "github_user": "aivarsk",
    "github_project": "tuxedo-python",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "tuxedo"
}
        
Elapsed time: 0.04828s