From 709d0f9c51b2c4fdf97fc026ffc05635378f1449 Mon Sep 17 00:00:00 2001 From: Sharoon Thomas Date: Tue, 7 May 2024 16:58:51 +0200 Subject: [PATCH 1/6] Update to v3 serialization * v3 is lighter and now fully supported on the server side * deserialization is backwards compatible with v2 [sc-74126] --- fulfil_client/serialization.py | 167 ++++++++++++++++++--------------- setup.py | 1 + 2 files changed, 90 insertions(+), 78 deletions(-) diff --git a/fulfil_client/serialization.py b/fulfil_client/serialization.py index 5c97151..1e412c5 100644 --- a/fulfil_client/serialization.py +++ b/fulfil_client/serialization.py @@ -3,6 +3,8 @@ from decimal import Decimal from collections import namedtuple from functools import partial +import isodate + try: import simplejson as json except ImportError: @@ -10,7 +12,7 @@ import base64 -CONTENT_TYPE = 'application/vnd.fulfil.v2+json' +CONTENT_TYPE = "application/vnd.fulfil.v3+json" class JSONDecoder(object): @@ -23,69 +25,90 @@ def register(cls, klass, decoder): cls.decoders[klass] = decoder def __call__(self, dct): - if dct.get('__class__') in self.decoders: - return self.decoders[dct['__class__']](dct) + if dct.get("__class__") in self.decoders: + return self.decoders[dct["__class__"]](dct) return dct -JSONDecoder.register( - 'datetime', - lambda dct: datetime.datetime( - dct['year'], dct['month'], dct['day'], - dct['hour'], dct['minute'], dct['second'], dct['microsecond'] +def register_decoder(klass): + def decorator(decoder): + assert klass not in JSONDecoder.decoders + JSONDecoder.decoders[klass] = decoder + + return decorator + + +@register_decoder("datetime") +def datetime_decoder(v): + if v.get("iso_string"): + return isodate.parse_datetime(v["iso_string"]) + return datetime.datetime( + v["year"], + v["month"], + v["day"], + v["hour"], + v["minute"], + v["second"], + v["microsecond"], ) -) -JSONDecoder.register( - 'date', - lambda dct: datetime.date(dct['year'], dct['month'], dct['day']) -) -JSONDecoder.register( - 'time', - lambda dct: datetime.time( - dct['hour'], dct['minute'], dct['second'], dct['microsecond'] + + +@register_decoder("date") +def date_decoder(v): + if v.get("iso_string"): + return isodate.parse_date(v["iso_string"]) + return datetime.date( + v["year"], + v["month"], + v["day"], ) -) -JSONDecoder.register( - 'timedelta', - lambda dct: datetime.timedelta(seconds=dct['seconds']) -) -def _bytes_decoder(dct): +@register_decoder("time") +def time_decoder(v): + if v.get("iso_string"): + return isodate.parse_time(v["iso_string"]) + return datetime.time(v["hour"], v["minute"], v["second"], v["microsecond"]) + + +@register_decoder("timedelta") +def timedelta_decoder(v): + if v.get("iso_string"): + return isodate.parse_duration(v["iso_string"]) + return datetime.timedelta(seconds=v["seconds"]) + + +@register_decoder("bytes") +def _bytes_decoder(v): cast = bytearray if bytes == str else bytes - return cast(base64.b64decode(dct['base64'].encode('utf-8'))) + return cast(base64.decodebytes(v["base64"].encode("utf-8"))) -JSONDecoder.register('bytes', _bytes_decoder) -JSONDecoder.register( - 'Decimal', lambda dct: Decimal(dct['decimal']) -) +@register_decoder("Decimal") +def decimal_decoder(v): + return Decimal(v["decimal"]) -dummy_record = namedtuple('Record', ['model_name', 'id', 'rec_name']) -JSONDecoder.register( - 'Model', lambda dct: dummy_record( - dct['model_name'], dct['id'], dct.get('rec_name') - ) -) +dummy_record = namedtuple("Record", ["model_name", "id", "rec_name"]) + +@register_decoder("Model") +def model_decoder(dct): + return dummy_record(dct["model_name"], dct["id"], dct.get("rec_name")) + +@register_decoder("AsyncResult") def parse_async_result(dct): from .client import AsyncResult - return AsyncResult(dct['task_id'], dct['token'], None) - -JSONDecoder.register( - 'AsyncResult', parse_async_result -) + return AsyncResult(dct["task_id"], dct["token"], None) class JSONEncoder(json.JSONEncoder): - serializers = {} def __init__(self, *args, **kwargs): - super(JSONEncoder, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # Force to use our custom decimal with simplejson self.use_decimal = False @@ -95,63 +118,51 @@ def register(cls, klass, encoder): cls.serializers[klass] = encoder def default(self, obj): - marshaller = self.serializers.get( - type(obj), - super(JSONEncoder, self).default - ) + marshaller = self.serializers.get(type(obj), super().default) return marshaller(obj) JSONEncoder.register( datetime.datetime, lambda o: { - '__class__': 'datetime', - 'year': o.year, - 'month': o.month, - 'day': o.day, - 'hour': o.hour, - 'minute': o.minute, - 'second': o.second, - 'microsecond': o.microsecond, - 'iso_string': o.isoformat(), - }) + "__class__": "datetime", + "iso_string": o.isoformat(), + }, +) JSONEncoder.register( datetime.date, lambda o: { - '__class__': 'date', - 'year': o.year, - 'month': o.month, - 'day': o.day, - 'iso_string': o.isoformat(), - }) + "__class__": "date", + "iso_string": o.isoformat(), + }, +) JSONEncoder.register( datetime.time, lambda o: { - '__class__': 'time', - 'hour': o.hour, - 'minute': o.minute, - 'second': o.second, - 'microsecond': o.microsecond, - }) + "__class__": "time", + "iso_string": o.isoformat(), + }, +) JSONEncoder.register( datetime.timedelta, lambda o: { - '__class__': 'timedelta', - 'seconds': o.total_seconds(), - }) -_bytes_encoder = lambda o: { - '__class__': 'bytes', - 'base64': base64.encodestring(o).decode('utf-8'), - } + "__class__": "timedelta", + "iso_string": isodate.duration_isoformat(o), + }, +) +_bytes_encoder = lambda o: { # noqa + "__class__": "bytes", + "base64": base64.b64encode(o).decode("utf-8"), +} JSONEncoder.register(bytes, _bytes_encoder) JSONEncoder.register(bytearray, _bytes_encoder) JSONEncoder.register( Decimal, lambda o: { - '__class__': 'Decimal', - 'decimal': str(o), - }) - + "__class__": "Decimal", + "decimal": str(o), + }, +) dumps = partial(json.dumps, cls=JSONEncoder) diff --git a/setup.py b/setup.py index b22b499..38e2f23 100755 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ 'babel', 'six', 'more-itertools', + 'isodate', ] From b6523d28b387f6d016b8f7a94eb1ad9764bc098d Mon Sep 17 00:00:00 2001 From: Sharoon Thomas Date: Tue, 7 May 2024 17:17:09 +0200 Subject: [PATCH 2/6] Add ci.yml and add tests for serialization [sc-74126] --- .github/workflows/ci.yml | 50 ++++++++++++ requirements_dev.txt | 1 + tests/test_serialization.py | 152 ++++++++++++++++++++++++++++++++++++ 3 files changed, 203 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 tests/test_serialization.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b7f17f1 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +name: CI (pip) +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: psf/black@stable + - uses: chartboost/ruff-action@v1 + build: + strategy: + matrix: + python-version: ["3.11"] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements_dev.txt + - name: Install from source (required for the pre-commit tests) + run: pip install . + - name: Test with pytest + run: pytest --cov=./ --cov-report=xml tests/test_serialization.py + release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Set up Python 3.11 + uses: actions/setup-python@v1 + with: + python-version: 3.11 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements_dev.txt + make dist + - name: Publish package + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@master + with: + user: __token__ + password: ${{ secrets.pypi_password }} diff --git a/requirements_dev.txt b/requirements_dev.txt index 837b7af..78f4562 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,4 @@ pytest bumpversion mock +redis diff --git a/tests/test_serialization.py b/tests/test_serialization.py new file mode 100644 index 0000000..7b07fbd --- /dev/null +++ b/tests/test_serialization.py @@ -0,0 +1,152 @@ +# -*- coding: UTF-8 -*- +from datetime import datetime, date, time, timedelta +from decimal import Decimal +import json +from fulfil_client.serialization import ( + dumps, + loads, + JSONDecoder, + JSONEncoder, +) + +import pytest + +# name, python object, v2 serialization, v3 serialization +SPECIFICATION = { + "datetime": { + "python_object": datetime(2020, 1, 1, 10, 20, 30, 10), + "v1": { + "__class__": "datetime", + "year": 2020, + "month": 1, + "day": 1, + "hour": 10, + "minute": 20, + "second": 30, + "microsecond": 10, + }, + "v2": { + "__class__": "datetime", + "year": 2020, + "month": 1, + "day": 1, + "hour": 10, + "minute": 20, + "second": 30, + "microsecond": 10, + "iso_string": "2020-01-01T10:20:30.000010", + }, + "v3": { + "__class__": "datetime", + "iso_string": "2020-01-01T10:20:30.000010", + }, + }, + "date": { + "python_object": date(2020, 1, 1), + "v1": { + "__class__": "date", + "year": 2020, + "month": 1, + "day": 1, + }, + "v2": { + "__class__": "date", + "year": 2020, + "month": 1, + "day": 1, + "iso_string": "2020-01-01", + }, + "v3": { + "__class__": "date", + "iso_string": "2020-01-01", + }, + }, + "time": { + "python_object": time(10, 20, 30, 15), + "v1": { + "__class__": "time", + "hour": 10, + "minute": 20, + "second": 30, + "microsecond": 15, + }, + "v2": { + "__class__": "time", + "hour": 10, + "minute": 20, + "second": 30, + "microsecond": 15, + "iso_string": "10:20:30.000015", + }, + "v3": { + "__class__": "time", + "iso_string": "10:20:30.000015", + }, + }, + "timedelta": { + "python_object": timedelta(hours=25), + "v1": { + "__class__": "timedelta", + "seconds": 90000, + }, + "v2": { + "__class__": "timedelta", + "seconds": 90000, + "iso_string": "P1DT1H", + }, + "v3": { + "__class__": "timedelta", + "iso_string": "P1DT1H", + }, + }, + "decimal": { + "python_object": Decimal("101.123456789"), + "v1": { + "__class__": "Decimal", + "decimal": "101.123456789", + }, + "v2": { + "__class__": "Decimal", + "decimal": "101.123456789", + }, + "v3": { + "__class__": "Decimal", + "decimal": "101.123456789", + }, + }, +} + + +PARAMS = [] +V3_PARAMS = [] +for klass, spec in SPECIFICATION.items(): + PARAMS.extend( + [ + pytest.param(spec["python_object"], spec["v1"], id="{}.v1".format(klass)), + pytest.param(spec["python_object"], spec["v2"], id="{}.v2".format(klass)), + pytest.param(spec["python_object"], spec["v3"], id="{}.v3".format(klass)), + ] + ) + V3_PARAMS.append( + pytest.param(spec["python_object"], spec["v3"], id="{}.v3".format(klass)), + ) + + +@pytest.mark.parametrize("python_object,serialized_object", V3_PARAMS) +def test_serialization_v3(python_object, serialized_object): + """ + Test the serialization v3 works + """ + # Create a dict from the serialized representation of the fulfil object + deserialized_hash = json.loads(dumps(python_object)) + for key, value in serialized_object.items(): + assert key in deserialized_hash + assert deserialized_hash[key] == value + + +@pytest.mark.parametrize("python_object,serialized_object", PARAMS) +def test_deserialization(python_object, serialized_object): + """ + Deserializing the object should return the python object + """ + assert python_object == loads(json.dumps(serialized_object)) From 59db85fba485461b8f0dbae16d8ea531a642cc80 Mon Sep 17 00:00:00 2001 From: Sharoon Thomas Date: Tue, 7 May 2024 17:26:00 +0200 Subject: [PATCH 3/6] Format and upgrade codebase --- .github/workflows/ci.yml | 1 - docs/conf.py | 135 +++++++------- examples/contacts-and-addresses.py | 116 ------------ examples/create-products.py | 81 --------- examples/create-sale-order.py | 280 ----------------------------- examples/fulfil_curlify.py | 35 ---- examples/sale.py | 80 --------- fulfil_client/__init__.py | 11 +- fulfil_client/client.py | 269 +++++++++++++-------------- fulfil_client/contrib/kombu.py | 5 +- fulfil_client/contrib/mail.py | 36 ++-- fulfil_client/contrib/mocking.py | 11 +- fulfil_client/exceptions.py | 7 +- fulfil_client/model.py | 147 +++++++-------- fulfil_client/oauth.py | 21 +-- fulfil_client/serialization.py | 1 - fulfil_client/signals.py | 38 ++-- setup.py | 60 +++---- tests/conftest.py | 10 +- tests/test_data_structures.py | 108 +++++------ tests/test_fulfil_client.py | 101 ++++------- tests/test_mocking.py | 50 +++--- travis_pypi_setup.py | 122 ------------- 23 files changed, 474 insertions(+), 1251 deletions(-) delete mode 100644 examples/contacts-and-addresses.py delete mode 100644 examples/create-products.py delete mode 100644 examples/create-sale-order.py delete mode 100644 examples/fulfil_curlify.py delete mode 100644 examples/sale.py delete mode 100755 travis_pypi_setup.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7f17f1..d11c8a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: psf/black@stable - uses: chartboost/ruff-action@v1 build: strategy: diff --git a/docs/conf.py b/docs/conf.py index d6f6db9..9918554 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,7 +20,7 @@ # directory, add these directories to sys.path here. If the directory is # relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # Get the project root dir, which is the parent dir of this cwd = os.getcwd() @@ -36,27 +36,27 @@ # -- General configuration --------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'fulfil_client' -copyright = u'2016, Fulfil.IO Inc.' +project = "fulfil_client" +copyright = "2016, Fulfil.IO Inc." # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout @@ -69,126 +69,126 @@ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to # some non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built # documents. -#keep_warnings = False +# keep_warnings = False # -- Options for HTML output ------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a # theme further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as # html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the # top of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon # of the docs. This file should be a Windows icon file (.ico) being # 16x16 or 32x32 pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) # here, relative to this directory. They are copied after the builtin # static files, so a file named "default.css" will overwrite the builtin # "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names # to template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. # Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. # Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages # will contain a tag referring to it. The value of this option # must be the base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'fulfil_clientdoc' +htmlhelp_basename = "fulfil_clientdoc" # -- Options for LaTeX output ------------------------------------------ @@ -196,10 +196,8 @@ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. #'preamble': '', } @@ -208,30 +206,34 @@ # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ - ('index', 'fulfil_client.tex', - u'fulfil_client Documentation', - u'Fulfil.IO Inc.', 'manual'), + ( + "index", + "fulfil_client.tex", + "fulfil_client Documentation", + "Fulfil.IO Inc.", + "manual", + ), ] # The name of an image file (relative to this directory) to place at # the top of the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings # are parts, not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output ------------------------------------ @@ -239,13 +241,11 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'fulfil_client', - u'fulfil_client Documentation', - [u'Fulfil.IO Inc.'], 1) + ("index", "fulfil_client", "fulfil_client Documentation", ["Fulfil.IO Inc."], 1) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ---------------------------------------- @@ -254,22 +254,25 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'fulfil_client', - u'fulfil_client Documentation', - u'Fulfil.IO Inc.', - 'fulfil_client', - 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "fulfil_client", + "fulfil_client Documentation", + "Fulfil.IO Inc.", + "fulfil_client", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False diff --git a/examples/contacts-and-addresses.py b/examples/contacts-and-addresses.py deleted file mode 100644 index ae2d37e..0000000 --- a/examples/contacts-and-addresses.py +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from fulfil_client import Client -client = Client('', '') - - -# ================= -# Creating Contacts -# ================= - -Contact = client.model('party.party') -contact, = Contact.create([{'name': 'Jon Doe'}]) - -# You can create multiple contacts in one request too ;-) -contacts = Contact.create([ - { - 'name': 'Jon Doe' - }, { - 'name': 'Matt Bower' - }, { - 'name': 'Joe Blow' - } -]) - -# ================ -# Creating Address -# ================ - -# Note: You need a contact id first to create an address -# -# Add an address to the contact created above -Address = client.model('party.address') -address, = Address.create([{ - 'party': contact['id'], - 'name': 'Jone Doe Apartment', - 'street': '9805 Kaiden Grove', - 'city': 'New Leland', - 'zip': '57726', -}]) - -# Address with country and subdivision - you first need to fetch the -# id of country and subdivision. -Country = client.model('country.country') -Subdivision = client.model('country.subdivision') - -country_usa, = Country.find([('code', '=', 'US')]) -state_california, = Subdivision.find([('code', '=', 'US-CA')]) - -address, = Address.create([{ - 'party': contact['id'], - 'name': 'Jone Doe Apartment', - 'street': '9805 Kaiden Grove', - 'city': 'New Leland', - 'zip': '57726', - 'country': country_usa['id'], - 'subdivision': state_california['id'], -}]) - - -# =========================== -# Creating Contact Mechanism -# =========================== - -# Creating a phone number for contact -ContactMechanism = client.model('party.contact_mechanism') - -phone, = ContactMechanism.create([{ - 'party': contact['id'], - 'type': 'phone', - 'value': '1321322143', -}]) - -# Creating an email address for contact -email, = ContactMechanism.create([{ - 'party': contact['id'], - 'type': 'email', - 'value': 'hola@jondoe@example.com', -}]) - - -# ============================ -# Creating Contacts (Advanced) -# ============================ - -# Creating a contact with address and contact mechanisms -contact, = Contact.create([{ - 'name': 'Jon Doe', - 'addresses': [('create', [{ - 'name': 'Jone Doe Apartment', - 'street': '9805 Kaiden Grove', - 'city': 'New Leland', - 'zip': '57726', - 'country': country_usa['id'], - 'subdivision': state_california['id'] - }])], - 'contact_mechanisms': [('create', [{ - 'type': 'phone', - 'value': '243243234' - }, { - 'email': 'email', - 'value': 'hello@jondoe.com' - }])] -}]) - - -# =================== -# Searching a Contact -# =================== - - -# Search contact by name -print Contact.find([('name', '=', 'Jon Doe')]) - -# Get a contact by ID -print Contact.get(contact['id']) diff --git a/examples/create-products.py b/examples/create-products.py deleted file mode 100644 index 4d25f5c..0000000 --- a/examples/create-products.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from decimal import Decimal - -from fulfil_client import Client -client = Client('', '') - - -# ========================== -# Creating Product Template -# ========================== - -Template = client.model('product.template') - -iphone, = Template.create([{ - 'name': 'iPhone', - 'account_category': True, -}]) - -# ================= -# Creating Products -# ================= - -Product = client.model('product.product') - -iphone6, = Product.create([{ - 'template': iphone['id'], - 'variant_name': 'iPhone 6', - 'code': 'IPHONE-6', - 'list_price': Decimal('699'), - 'cost_price': Decimal('599'), -}]) - -# Another variation -iphone6s, = Product.create([{ - 'template': iphone['id'], - 'variant_name': 'iPhone 6S', - 'code': 'IPHONE-6S', - 'list_price': Decimal('899'), - 'cost_price': Decimal('699'), -}]) - - -# ============================ -# Creating Products (Advanced) -# ============================ - -# Create template and products in single call! -print Template.create([{ - 'name': 'iPhone', - 'account_category': True, - 'products': [('create', [{ - 'variant_name': 'iPhone 6', - 'code': 'IPHONE-6', - 'list_price': Decimal('699'), - 'cost_price': Decimal('599'), - - }, { - 'variant_name': 'iPhone 6S', - 'code': 'IPHONE-6S', - 'list_price': Decimal('899'), - 'cost_price': Decimal('699'), - }])] -}]) - - -# ====================== -# Searching for Products -# ====================== - -# Search by SKU(exact match) -print Product.find([('code', '=', 'IPHONE-6')]) - -# Search by SKU(pattern match) -print Product.find([('code', 'ilike', '%IPHONE%')]) - -# Search by name(pattern match, case insensitive) -print Product.find([('name', 'ilike', '%Phone%')]) - -# Get a product by ID -print Product.get(iphone6s['id']) diff --git a/examples/create-sale-order.py b/examples/create-sale-order.py deleted file mode 100644 index 6695716..0000000 --- a/examples/create-sale-order.py +++ /dev/null @@ -1,280 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -This is a complete example where you have to push an order to Fulfil.IO. The -steps are: - - 1. Fetch inventory for the products that have been sold - 2. Create new customer, address - 3. Process the order. -""" -from datetime import date -from decimal import Decimal - -from fulfil_client import Client -client = Client('', '') - - -def get_warehouses(): - """ - Return the warehouses in the system - """ - StockLocation = client.model('stock.location') - return StockLocation.find( - [('type', '=', 'warehouse')], # filter just warehouses - fields=['code', 'name'] # Get the code and name fields - ) - - -def get_product_inventory(product_id, warehouse_ids): - """ - Return the product inventory in each location. The returned response - will look like:: - - { - 12: { // Product ID - 4: { // Location ID - 'quantity_on_hand': 12.0, - 'quantity_available': 8.0 - }, - 5: { // Location ID - 'quantity_on_hand': 8.0, - 'quantity_available': 8.0 - }, - }, - 126: { // Product ID - 4: { // Location ID - 'quantity_on_hand': 16.0, - 'quantity_available': 15.0 - }, - 5: { // Location ID - 'quantity_on_hand': 9.0, - 'quantity_available': 8.0 - }, - } - } - - Read more: - http://docs.fulfiliorestapi.apiary.io/#reference/product/product-inventory - """ - Product = client.model('product.product') - - return Product.get_product_inventory( - [product_id], warehouse_ids - )[product_id] - - -def get_customer(code): - """ - Fetch a customer with the code. - Returns None if the customer is not found. - """ - Party = client.model('party.party') - results = Party.find([('code', '=', code)]) - if results: - return results[0]['id'] - - -def get_address(customer_id, data): - """ - Easier to fetch the addresses of customer and then check one by one. - - You can get fancy by using some validation mechanism too - """ - Address = client.model('party.address') - - addresses = Address.find( - [('party', '=', customer_id)], - fields=[ - 'name', 'street', 'street_bis', 'city', 'zip', - 'subdivision.code', 'country.code' - ] - ) - for address in addresses: - if ( - address['name'] == data['name'] and - address['street'] == data['street'] and - address['street_bis'] == data['street_bis'] and - address['city'] == data['city'] and - address['zip'] == data['zip'] and - address['subdivision.code'].endswith(data['state']) and - address['country.code'] == data['country']): - return address['id'] - - -def create_address(customer_id, data): - """ - Create an address and return the id - """ - Address = client.model('party.address') - Country = client.model('country.country') - Subdivision = client.model('country.subdivision') - - country, = Country.find([('code', '=', data['country'])]) - state, = Subdivision.find([ - ('code', 'ilike', '%-' + data['state']), # state codes are US-CA, IN-KL - ('country', '=', country['id']) - ]) - - address, = Address.create([{ - 'party': customer_id, - 'name': data['name'], - 'street': data['street'], - 'street_bis': data['street_bis'], - 'city': data['city'], - 'zip': data['zip'], - 'country': country['id'], - 'subdivision': state['id'], - }]) - return address['id'] - - -def create_customer(name, email, phone): - """ - Create a customer with the name. - Then attach the email and phone as contact methods - """ - Party = client.model('party.party') - ContactMechanism = client.model('party.contact_mechanism') - - party, = Party.create([{'name': name}]) - - # Bulk create the email and phone - ContactMechanism.create([ - {'type': 'email', 'value': email, 'party': party}, - {'type': 'phone', 'value': phone, 'party': party}, - ]) - - return party - - -def get_product(code): - """ - Given a product code/sku return the product id - """ - Product = client.model('product.product') - return Product.find( - [('code', '=', code)], # Filter - fields=['code', 'variant_name', 'cost_price'] - )[0] - - -def create_order(order): - """ - Create an order on fulfil from order_details. - See the calling function below for an example of the order_details - """ - SaleOrder = client.model('sale.sale') - SaleOrderLine = client.model('sale.line') - - # Check if customer exists, if not create one - customer_id = get_customer(order['customer']['code']) - if not customer_id: - customer_id = create_customer( - order['customer']['name'], - order['customer']['email'], - order['customer']['phone'], - ) - - # No check if there is a matching address - invoice_address = get_address( - customer_id, - order['invoice_address'] - ) - if not invoice_address: - invoice_address = create_address( - customer_id, - order['invoice_address'] - ) - - # See if the shipping address exists, if not create it - shipment_address = get_address( - customer_id, - order['shipment_address'] - ) - if not shipment_address: - shipment_address = create_address( - customer_id, - order['shipment_address'] - ) - - sale_order_id, = SaleOrder.create([{ - 'reference': order['number'], - 'sale_date': order['date'], - 'party': customer_id, - 'invoice_address': invoice_address, - 'shipment_address': shipment_address, - }]) - - # fetch inventory of all the products before we create lines - warehouses = get_warehouses() - warehouse_ids = [warehouse['id'] for warehouse in warehouses] - - lines = [] - for item in order['items']: - # get the product. We assume ti already exists. - product = get_product(item['product']) - - # find the first location that has inventory - product_inventory = get_product_inventory(product, warehouse_ids) - for location, quantities in product_inventory.items(): - if quantities['quantity_available'] >= item['quantity']: - break - - lines.append({ - 'sale': sale_order_id, - 'product': product, - 'quantity': item['quantity'], - 'unit_price': item['unit_price'], - 'warehouse': location, - }) - - SaleOrderLine.create(lines) - - SaleOrder.quote([sale_order_id]) - SaleOrder.confirm([sale_order_id]) - - -if __name__ == '__main__': - create_order({ - 'customer': { - 'code': 'A1234', - 'name': 'Sharoon Thomas', - 'email': 'st@fulfil.io', - 'phone': '650-999-9999', - }, - 'number': 'SO-12345', # an order number - 'date': date.today(), # An order date - 'invoice_address': { - 'name': 'Sharoon Thomas', - 'street': '444 Castro St.', - 'street2': 'STE 1200', - 'city': 'Mountain View', - 'zip': '94040', - 'state': 'CA', - 'country': 'US', - }, - 'shipment_address': { - 'name': 'Office Manager', - 'street': '444 Castro St.', - 'street2': 'STE 1200', - 'city': 'Mountain View', - 'zip': '94040', - 'state': 'CA', - 'country': 'US', - }, - 'items': [ - { - 'product': 'P123', - 'quantity': 2, - 'unit_price': Decimal('99'), - 'description': 'P123 is a fabulous product', - }, - { - 'product': 'P456', - 'quantity': 1, - 'unit_price': Decimal('100'), - 'description': 'Yet another amazing product', - }, - ] - }) diff --git a/examples/fulfil_curlify.py b/examples/fulfil_curlify.py deleted file mode 100644 index ad64389..0000000 --- a/examples/fulfil_curlify.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Translate fulfil requests to curl. - -Need to have the following installed - -pip install curlify blinker -""" -import os -import curlify -from fulfil_client import Client -from fulfil_client.signals import response_received, signals_available - -fulfil = Client(os.environ['FULFIL_SUBDOMAIN'], os.environ['FULFIL_API_KEY']) - -print("Signal Available?:", signals_available) - -Product = fulfil.model('product.product') - -products = Product.find([]) - -@response_received.connect -def curlify_response(response): - print('=' * 80) - print(curlify.to_curl(response.request)) - print('=' * 80) - print(response.content) - print('=' * 80) - - -print Product.get_next_available_date( - products[0]['id'], - 1, - 4, - True -) diff --git a/examples/sale.py b/examples/sale.py deleted file mode 100644 index aff1f4e..0000000 --- a/examples/sale.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -from fulfil_client import Client -client = Client('', '') - - -# ============= -# Creating Sale -# ============= - -# Sale requires customer(contact) and address id. -Contact = client.model('party.party') -Sale = client.model('sale.sale') - -# Get the contact first -contacts = Contact.find([('name', 'ilike', '%Jon%')]) -contact, = Contact.get(contacts[0]['id']) - -sale, = Sale.create([{ - 'party': contact['id'], - 'shipment_address': contact['addresses'][0], - 'invoice_address': contact['addresses'][0], -}]) - -# =========================== -# Adding items(line) to Sale -# =========================== - -Product = client.model('product.product') -Line = client.model('sale.line') - -products = Product.find([('code', '=', 'IPHONE-6')]) -iphone6, = Product.get(products[0]['id']) - -products = Product.find([('code', '=', 'IPHONE-6S')]) -iphone6s, = Product.get(products[0]['id']) - - -line1, = Line.create([{ - 'sale': sale['id'], - 'product': iphone6['id'], - 'description': iphone6['rec_name'], - 'unit': iphone6['default_uom'], - 'unit_price': iphone6['list_price'], - 'quantity': 3 -}]) - -line2, = Line.create([{ - 'sale': sale['id'], - 'product': iphone6s['id'], - 'description': iphone6s['rec_name'], - 'unit': iphone6s['default_uom'], - 'unit_price': iphone6s['list_price'], - 'quantity': 1 - -}]) - -# ============================ -# Creating Sale (Advanced) -# ============================ - -# Create sale with lines in single call! -sale, = Sale.create([{ - 'party': contact['id'], - 'shipment_address': contact['addresses'][0], - 'invoice_address': contact['addresses'][0], - 'lines': [('create', [{ - 'product': iphone6['id'], - 'description': iphone6['rec_name'], - 'unit': iphone6['default_uom'], - 'unit_price': iphone6['list_price'], - 'quantity': 3 - }, { - 'product': iphone6s['id'], - 'description': iphone6s['rec_name'], - 'unit': iphone6['default_uom'], - 'unit_price': iphone6s['list_price'], - 'quantity': 1 - }])] -}]) diff --git a/fulfil_client/__init__.py b/fulfil_client/__init__.py index ffe1346..32c54bb 100755 --- a/fulfil_client/__init__.py +++ b/fulfil_client/__init__.py @@ -1,13 +1,10 @@ # -*- coding: utf-8 -*- -__author__ = 'Fulfil.IO Inc.' -__email__ = 'hello@fulfil.io' -__version__ = '2.0.0' +__author__ = "Fulfil.IO Inc." +__email__ = "hello@fulfil.io" +__version__ = "2.0.0" # flake8: noqa -from .client import ( - Client, Model, SessionAuth, APIKeyAuth, BearerAuth, - verify_webhook -) +from .client import Client, Model, SessionAuth, APIKeyAuth, BearerAuth, verify_webhook from .exceptions import ClientError, UserError, ServerError diff --git a/fulfil_client/client.py b/fulfil_client/client.py index 35a90e6..d9b4052 100755 --- a/fulfil_client/client.py +++ b/fulfil_client/client.py @@ -13,13 +13,17 @@ from more_itertools import chunked from .serialization import dumps, loads from .exceptions import ( - UserError, ClientError, ServerError, AuthenticationError, RateLimitError + UserError, + ClientError, + ServerError, + AuthenticationError, + RateLimitError, ) from .signals import response_received from .exceptions import Error # noqa -request_logger = logging.getLogger('fulfil_client.request') +request_logger = logging.getLogger("fulfil_client.request") def json_response(function): @@ -30,13 +34,13 @@ def wrapper(*args, **kwargs): if rv.status_code == 400: # Usually an user error error = loads(rv.text) - if error.get('type') == 'UserError': + if error.get("type") == "UserError": # These are error messages meant to be displayed to the # user. raise UserError( - message=error.get('message'), - code=error.get('code'), - description=error.get('description'), + message=error.get("message"), + code=error.get("code"), + description=error.get("description"), ) else: # Some unknown error type. Raise a generic client error @@ -53,24 +57,23 @@ def wrapper(*args, **kwargs): # 4XX range errors always have a JSON response # with a code, message and description. error = rv.text - if rv.headers.get('Content-Type') == 'application/json': - error = loads(rv.text).get('message', error) - raise ClientError( - error, - rv.status_code - ) + if rv.headers.get("Content-Type") == "application/json": + error = loads(rv.text).get("message", error) + raise ClientError(error, rv.status_code) else: # 5XX Internal Server errors raise ServerError( - rv.text, rv.status_code, rv.headers.get('X-Sentry-ID') + rv.text, rv.status_code, rv.headers.get("X-Sentry-ID") ) return loads(rv.text) + return wrapper class SessionAuth(requests.auth.AuthBase): "Session Authentication" - type_ = 'Session' + + type_ = "Session" def __init__(self, login, user_id, session): self.login = login @@ -78,50 +81,57 @@ def __init__(self, login, user_id, session): self.session = session def __call__(self, r): - r.headers['Authorization'] = 'Session ' + base64.b64encode( - '%s:%s:%s' % (self.login, self.user_id, self.session) + r.headers["Authorization"] = "Session " + base64.b64encode( + "%s:%s:%s" % (self.login, self.user_id, self.session) ) return r class BearerAuth(requests.auth.AuthBase): "Bearer Authentication" - type_ = 'BearerAuth' + + type_ = "BearerAuth" def __init__(self, access_token): self.access_token = access_token def __call__(self, r): - r.headers['Authorization'] = 'Bearer ' + self.access_token + r.headers["Authorization"] = "Bearer " + self.access_token return r class APIKeyAuth(requests.auth.AuthBase): "API key based Authentication" - type_ = 'APIKey' + + type_ = "APIKey" def __init__(self, api_key): self.api_key = api_key def __call__(self, r): - r.headers['x-api-key'] = self.api_key + r.headers["x-api-key"] = self.api_key return r class Client(object): - - def __init__(self, subdomain, - api_key=None, context=None, auth=None, - user_agent="Python Client", base_url="fulfil.io", - retry_on_rate_limit=False): + def __init__( + self, + subdomain, + api_key=None, + context=None, + auth=None, + user_agent="Python Client", + base_url="fulfil.io", + retry_on_rate_limit=False, + ): self.subdomain = subdomain - if self.subdomain == 'localhost': - self.host = 'http://localhost:8000' + if self.subdomain == "localhost": + self.host = "http://localhost:8000" else: - self.host = 'https://{}.{}'.format(self.subdomain, base_url) + self.host = "https://{}.{}".format(self.subdomain, base_url) - self.base_url = '%s/api/v2' % self.host + self.base_url = "%s/api/v2" % self.host self.session = requests.Session() if api_key is not None: @@ -136,15 +146,17 @@ def __init__(self, subdomain, read=retries, connect=retries, backoff_factor=0.5, - status_forcelist=(429, ), + status_forcelist=(429,), ) adapter = HTTPAdapter(max_retries=retry) - self.session.mount('http://', adapter) - self.session.mount('https://', adapter) + self.session.mount("http://", adapter) + self.session.mount("https://", adapter) - self.session.headers.update({ - 'User-Agent': user_agent, - }) + self.session.headers.update( + { + "User-Agent": user_agent, + } + ) self.context = {} if context is not None: @@ -159,24 +171,22 @@ def set_auth(self, auth): if auth is None: return if isinstance(auth, BearerAuth): - self.base_url = '%s/api/v2' % self.host + self.base_url = "%s/api/v2" % self.host def set_user_agent(self, user_agent): - self.session.headers.update({ - 'User-Agent': user_agent - }) + self.session.headers.update({"User-Agent": user_agent}) def refresh_context(self): """ Get the default context of the user and save it """ - User = self.model('res.user') + User = self.model("res.user") self.context = User.get_preferences(True) return self.context def today(self): - Date = self.model('ir.date') + Date = self.model("ir.date") rv = Date.today() return rv @@ -208,25 +218,20 @@ def login(self, login, password, set_auth=False): """ rv = self.session.post( self.host, - dumps({ - "method": "common.db.login", - "params": [login, password] - }), + dumps({"method": "common.db.login", "params": [login, password]}), ) - rv = loads(rv.content)['result'] + rv = loads(rv.content)["result"] if set_auth: - self.set_auth( - SessionAuth(login, *rv) - ) + self.set_auth(SessionAuth(login, *rv)) return rv def is_auth_alive(self): "Return true if the auth is not expired, else false" - model = self.model('ir.model') + model = self.model("ir.model") try: model.search([], None, 1, None) except ClientError as err: - if err and err.message['code'] == 403: + if err and err.message["code"] == 403: return False raise except Exception: @@ -236,8 +241,8 @@ def is_auth_alive(self): class WizardSession(object): - """An object to represent a specific session - """ + """An object to represent a specific session""" + def __init__(self, wizard, context): self.wizard = wizard @@ -263,23 +268,16 @@ def execute(self, state, context=None): self.state = state while self.state != self.end_state: result = self.parse_result( - self.wizard.execute( - self.session_id, - self.data, - self.state, - ctx - ) + self.wizard.execute(self.session_id, self.data, self.state, ctx) ) - if 'view' in result: + if "view" in result: return result return result def parse_result(self, result): - if 'view' in result: - view = result['view'] - self.data[view['state']].update( - view['defaults'] - ) + if "view" in result: + view = result["view"] + self.data[view["state"]].update(view["defaults"]) else: self.state = self.end_state return result @@ -292,11 +290,10 @@ def delete(self): class Wizard(object): - def __init__(self, client, wizard_name, **kwargs): self.client = client self.wizard_name = wizard_name - self.context = kwargs.get('context', {}) + self.context = kwargs.get("context", {}) @contextmanager def session(self, **context): @@ -306,19 +303,17 @@ def session(self, **context): @property def path(self): - return '%s/wizard/%s' % (self.client.base_url, self.wizard_name) + return "%s/wizard/%s" % (self.client.base_url, self.wizard_name) @json_response def execute(self, session_id, data, state, context=None): ctx = self.client.context.copy() ctx.update(context or {}) - request_logger.debug( - "Wizard::%s.execute::%s" % (self.wizard_name, state) - ) + request_logger.debug("Wizard::%s.execute::%s" % (self.wizard_name, state)) rv = self.client.session.put( - self.path + '/execute', + self.path + "/execute", dumps([session_id, data, state]), - params={'context': dumps(ctx)} + params={"context": dumps(ctx)}, ) # Call response signal return rv @@ -329,9 +324,7 @@ def create(self, context=None): ctx.update(context or {}) request_logger.debug("Wizard::%s.create" % (self.wizard_name,)) rv = self.client.session.put( - self.path + '/create', - dumps([]), - params={'context': dumps(ctx)} + self.path + "/create", dumps([]), params={"context": dumps(ctx)} ) # Call response signal return rv @@ -339,10 +332,7 @@ def create(self, context=None): @json_response def delete(self, session_id): request_logger.debug("Wizard::%s.delete" % (self.wizard_name,)) - rv = self.client.session.put( - self.path + '/delete', - dumps([session_id]) - ) + rv = self.client.session.put(self.path + "/delete", dumps([session_id])) # Call response signal return rv @@ -369,7 +359,6 @@ def update(self, data=None, **kwargs): class Model(object): - def __init__(self, client, model_name): self.client = client self.model_name = model_name @@ -378,42 +367,42 @@ def __getattr__(self, name): @json_response def proxy_method(*args, **kwargs): context = self.client.context.copy() - context.update(kwargs.pop('context', {})) + context.update(kwargs.pop("context", {})) request_logger.debug( - "%s.%s::%s::%s" % ( - self.model_name, name, args, kwargs - ) + "%s.%s::%s::%s" % (self.model_name, name, args, kwargs) ) rv = self.client.session.put( - self.path + '/%s' % name, + self.path + "/%s" % name, dumps(args), params={ - 'context': dumps(context), - } + "context": dumps(context), + }, ) response_received.send(rv) return rv + return proxy_method @property def path(self): - return '%s/model/%s' % (self.client.base_url, self.model_name) + return "%s/model/%s" % (self.client.base_url, self.model_name) @json_response def get(self, id, context=None): ctx = self.client.context.copy() ctx.update(context or {}) rv = self.client.session.get( - self.path + '/%d' % id, + self.path + "/%d" % id, params={ - 'context': dumps(ctx), - } + "context": dumps(ctx), + }, ) response_received.send(rv) return rv - def search_read_all(self, domain, order, fields, batch_size=500, - context=None, offset=0, limit=None): + def search_read_all( + self, domain, order, fields, batch_size=500, context=None, offset=0, limit=None + ): """ An endless iterator that iterates over records. @@ -435,7 +424,12 @@ def search_read_all(self, domain, order, fields, batch_size=500, @json_response def find( - self, filter=None, page=1, per_page=10, fields=None, order=None, + self, + filter=None, + page=1, + per_page=10, + fields=None, + order=None, context=None, ): """ @@ -464,13 +458,13 @@ def find( rv = self.client.session.get( self.path, params={ - 'filter': dumps(filter or []), - 'page': page, - 'per_page': per_page, - 'field': fields, - 'order': dumps(order), - 'context': dumps(context or self.client.context), - } + "filter": dumps(filter or []), + "page": page, + "per_page": per_page, + "field": fields, + "order": dumps(order), + "context": dumps(context or self.client.context), + }, ) response_received.send(rv) return rv @@ -482,62 +476,58 @@ def attach(self, id, filename, url): :param filename: File name of attachment :param url: Public url to download file from. """ - Attachment = self.client.model('ir.attachment') + Attachment = self.client.model("ir.attachment") return Attachment.add_attachment_from_url( - filename, url, '%s,%s' % (self.model_name, id) + filename, url, "%s,%s" % (self.model_name, id) ) class Report(object): - def __init__(self, client, report_name): self.client = client self.report_name = report_name @property def path(self): - return '%s/report/%s' % (self.client.base_url, self.report_name) + return "%s/report/%s" % (self.client.base_url, self.report_name) @json_response def execute(self, records=None, data=None, **kwargs): context = self.client.context.copy() - context.update(kwargs.pop('context', {})) + context.update(kwargs.pop("context", {})) rv = self.client.session.put( self.path, json={ - 'objects': records or [], - 'data': data or {}, + "objects": records or [], + "data": data or {}, }, params={ - 'context': dumps(context), - } + "context": dumps(context), + }, ) response_received.send(rv) return rv class InteractiveReport(object): - def __init__(self, client, model_name): self.client = client self.model_name = model_name @property def path(self): - return '%s/model/%s/execute' % ( - self.client.base_url, self.model_name - ) + return "%s/model/%s/execute" % (self.client.base_url, self.model_name) @json_response def execute(self, **kwargs): context = self.client.context.copy() - context.update(kwargs.pop('context', {})) + context.update(kwargs.pop("context", {})) rv = self.client.session.put( self.path, dumps([kwargs]), params={ - 'context': dumps(context), - } + "context": dumps(context), + }, ) response_received.send(rv) return rv @@ -551,11 +541,11 @@ class AsyncResult(object): and result. """ - PENDING = 'PENDING' - STARTED = 'STARTED' - FAILURE = 'FAILURE' - SUCCESS = 'SUCCESS' - RETRY = 'RETRY' + PENDING = "PENDING" + STARTED = "STARTED" + FAILURE = "FAILURE" + SUCCESS = "SUCCESS" + RETRY = "RETRY" def __init__(self, task_id, token, client): self.task_id = task_id @@ -567,7 +557,7 @@ def __init__(self, task_id, token, client): @property def path(self): - return '%s/async-result' % (self.client.base_url) + return "%s/async-result" % (self.client.base_url) def bind(self, client): self.client = client @@ -582,11 +572,7 @@ def _fetch_result(self): ) rv = self.client.session.post( self.path, - json={ - 'tasks': [ - [self.task_id, self.token] - ] - }, + json={"tasks": [[self.task_id, self.token]]}, ) response_received.send(rv) return rv @@ -597,19 +583,17 @@ def refresh_if_needed(self): """ if self.state in (self.PENDING, self.STARTED): try: - response, = self._fetch_result()['tasks'] + (response,) = self._fetch_result()["tasks"] except (KeyError, ValueError): - raise Exception( - "Unable to find results for task." - ) + raise Exception("Unable to find results for task.") - if 'error' in response: + if "error" in response: self.state == self.FAILURE - raise ServerError(response['error']) + raise ServerError(response["error"]) - if 'state' in response: - self.state = response['state'] - self.result = response['result'] + if "state" in response: + self.state = response["state"] + self.result = response["result"] def failed(self): """ @@ -657,12 +641,7 @@ def verify_webhook(data, secret, hmac_header): :param hmac_header: Value of the header in the request """ digest = hmac.new( - base64.b64decode(secret), - data.encode('utf-8'), - hashlib.sha256 + base64.b64decode(secret), data.encode("utf-8"), hashlib.sha256 ).digest() computed_hmac = base64.b64encode(digest) - return hmac.compare_digest( - computed_hmac, - hmac_header.encode('utf-8') - ) + return hmac.compare_digest(computed_hmac, hmac_header.encode("utf-8")) diff --git a/fulfil_client/contrib/kombu.py b/fulfil_client/contrib/kombu.py index a483a5f..4b2b3b7 100644 --- a/fulfil_client/contrib/kombu.py +++ b/fulfil_client/contrib/kombu.py @@ -8,9 +8,8 @@ This is used in setuptools to register custom endpoint """ + from fulfil_client.serialization import dumps, loads, CONTENT_TYPE -register_args = ( - dumps, loads, CONTENT_TYPE, 'utf-8' -) +register_args = (dumps, loads, CONTENT_TYPE, "utf-8") diff --git a/fulfil_client/contrib/mail.py b/fulfil_client/contrib/mail.py index 8d2398a..19a2758 100644 --- a/fulfil_client/contrib/mail.py +++ b/fulfil_client/contrib/mail.py @@ -13,8 +13,14 @@ def render_email( - from_email, to, subject, text_template=None, html_template=None, - cc=None, attachments=None, **context + from_email, + to, + subject, + text_template=None, + html_template=None, + cc=None, + attachments=None, + **context, ): """ Read the templates for email messages, format them, construct @@ -39,18 +45,16 @@ def render_email( text_part = None if text_template: - text_part = MIMEText( - text_template.encode("utf-8"), 'plain', _charset="UTF-8") + text_part = MIMEText(text_template.encode("utf-8"), "plain", _charset="UTF-8") html_part = None if html_template: - html_part = MIMEText( - html_template.encode("utf-8"), 'html', _charset="UTF-8") + html_part = MIMEText(html_template.encode("utf-8"), "html", _charset="UTF-8") if text_part and html_part: # Construct an alternative part since both the HTML and Text Parts # exist. - message = MIMEMultipart('alternative') + message = MIMEMultipart("alternative") message.attach(text_part) message.attach(html_part) else: @@ -60,7 +64,7 @@ def render_email( if attachments: # If an attachment exists, the MimeType should be mixed and the # message body should just be another part of it. - message_with_attachments = MIMEMultipart('mixed') + message_with_attachments = MIMEMultipart("mixed") # Set the message body as the first part message_with_attachments.attach(message) @@ -69,32 +73,32 @@ def render_email( message = message_with_attachments for filename, content in attachments.items(): - part = MIMEBase('application', "octet-stream") + part = MIMEBase("application", "octet-stream") part.set_payload(content) Encoders.encode_base64(part) # XXX: Filename might have to be encoded with utf-8, # i.e., part's encoding or with email's encoding part.add_header( - 'Content-Disposition', 'attachment; filename="%s"' % filename + "Content-Disposition", 'attachment; filename="%s"' % filename ) message.attach(part) # If list of addresses are provided for to and cc, then convert it # into a string that is "," separated. if isinstance(to, (list, tuple)): - to = ', '.join(to) + to = ", ".join(to) if isinstance(cc, (list, tuple)): - cc = ', '.join(cc) + cc = ", ".join(cc) # We need to use Header objects here instead of just assigning the strings # in order to get our headers properly encoded (with QP). - message['Subject'] = Header(subject, 'ISO-8859-1') + message["Subject"] = Header(subject, "ISO-8859-1") # TODO handle case where domain contains non-ascii letters # https://docs.aws.amazon.com/ses/latest/APIReference/API_Destination.html - message['From'] = from_email - message['To'] = to + message["From"] = from_email + message["To"] = to if cc: - message['Cc'] = cc + message["Cc"] = cc return message diff --git a/fulfil_client/contrib/mocking.py b/fulfil_client/contrib/mocking.py index 07bea35..0b9bf59 100644 --- a/fulfil_client/contrib/mocking.py +++ b/fulfil_client/contrib/mocking.py @@ -10,10 +10,11 @@ class MockFulfil(object): A Mock object that helps mock away the Fulfil API for testing. """ + responses = [] models = {} context = {} - subdomain = 'mock-test' + subdomain = "mock-test" def __init__(self, target, responses=None): self.target = target @@ -31,9 +32,7 @@ def __exit__(self, type, value, traceback): return type is None def model(self, model_name): - return self.models.setdefault( - model_name, mock.MagicMock(name=model_name) - ) + return self.models.setdefault(model_name, mock.MagicMock(name=model_name)) def start(self): """ @@ -42,9 +41,7 @@ def start(self): self._patcher = mock.patch(target=self.target) MockClient = self._patcher.start() instance = MockClient.return_value - instance.model.side_effect = mock.Mock( - side_effect=self.model - ) + instance.model.side_effect = mock.Mock(side_effect=self.model) def stop(self): """ diff --git a/fulfil_client/exceptions.py b/fulfil_client/exceptions.py index b07b61f..3a82509 100644 --- a/fulfil_client/exceptions.py +++ b/fulfil_client/exceptions.py @@ -8,7 +8,10 @@ def __str__(self): return str(self.message) def __getnewargs__(self): - return (self.message, self.code,) + return ( + self.message, + self.code, + ) class ServerError(Error): @@ -37,6 +40,7 @@ class AuthenticationError(ClientError): This could be because a token expired or becuase the auth is just invalid. """ + pass @@ -48,6 +52,7 @@ class UserError(ClientError): to the user. User errors generally have a description too, so respect that too. """ + def __init__(self, message, code, description=None): self.description = description super(UserError, self).__init__(message, code) diff --git a/fulfil_client/model.py b/fulfil_client/model.py index 85b72c3..32b8703 100644 --- a/fulfil_client/model.py +++ b/fulfil_client/model.py @@ -5,6 +5,7 @@ A collection of model layer APIs to write lesser code and better """ + import six import logging import functools @@ -21,7 +22,7 @@ from future_builtins import zip -cache_logger = logging.getLogger('fulfil_client.cache') +cache_logger = logging.getLogger("fulfil_client.cache") class BaseType(object): @@ -61,60 +62,52 @@ def __delete__(self, instance): class IntType(BaseType): - def __init__(self, *args, **kwargs): - kwargs.setdefault('cast', int) + kwargs.setdefault("cast", int) super(IntType, self).__init__(*args, **kwargs) class BooleanType(BaseType): - def __init__(self, *args, **kwargs): - kwargs.setdefault('cast', bool) + kwargs.setdefault("cast", bool) super(BooleanType, self).__init__(*args, **kwargs) class StringType(BaseType): - def __init__(self, *args, **kwargs): - kwargs.setdefault('cast', six.text_type) + kwargs.setdefault("cast", six.text_type) super(StringType, self).__init__(*args, **kwargs) class DecimalType(BaseType): - def __init__(self, *args, **kwargs): - kwargs.setdefault('cast', Decimal) + kwargs.setdefault("cast", Decimal) super(DecimalType, self).__init__(*args, **kwargs) class FloatType(BaseType): - def __init__(self, *args, **kwargs): - kwargs.setdefault('cast', float) + kwargs.setdefault("cast", float) super(FloatType, self).__init__(*args, **kwargs) class DateTime(BaseType): - def __init__(self, *args, **kwargs): - kwargs.setdefault('cast', datetime) + kwargs.setdefault("cast", datetime) super(DateTime, self).__init__(*args, **kwargs) class Date(BaseType): - def __init__(self, *args, **kwargs): - kwargs.setdefault('cast', date) + kwargs.setdefault("cast", date) super(Date, self).__init__(*args, **kwargs) class One2ManyType(BaseType): - def __init__(self, model_name, cache=False, *args, **kwargs): self.model_name = model_name self.cache = cache - kwargs.setdefault('cast', list) + kwargs.setdefault("cast", list) super(One2ManyType, self).__init__(*args, **kwargs) def __get__(self, instance, owner): @@ -155,7 +148,7 @@ def __get__(self, instance, owner): return None return Money( instance._values.get(self.name, self.default), - getattr(instance, self.currency_field) + getattr(instance, self.currency_field), ) else: return self @@ -170,6 +163,7 @@ class ModelType(IntType): :param cache: If set, it looks up the record in the cache backend of the underlying model before querying the server to fetch records. """ + def __init__(self, model_name, cache=False, *args, **kwargs): self.model_name = model_name self.cache = cache @@ -192,27 +186,27 @@ class NamedDescriptorResolverMetaClass(type): """ def __new__(cls, classname, bases, class_dict): - abstract = class_dict.get('__abstract__', False) - model_name = class_dict.get('__model_name__') + abstract = class_dict.get("__abstract__", False) + model_name = class_dict.get("__model_name__") if not abstract and not model_name: for base in bases: - if hasattr(base, '__model_name__'): + if hasattr(base, "__model_name__"): model_name = base.__model_name__ break else: - raise Exception('__model_name__ not defined for model') + raise Exception("__model_name__ not defined for model") fields = set([]) eager_fields = set([]) for base in bases: - if hasattr(base, '_fields'): + if hasattr(base, "_fields"): fields |= set(base._fields) - if hasattr(base, '_eager_fields'): + if hasattr(base, "_eager_fields"): eager_fields |= set(base._eager_fields) - fields |= class_dict.get('_fields', set([])) - eager_fields |= class_dict.get('_eager_fields', set([])) + fields |= class_dict.get("_fields", set([])) + eager_fields |= class_dict.get("_eager_fields", set([])) # Iterate through the new class' __dict__ to: # @@ -226,8 +220,8 @@ def __new__(cls, classname, bases, class_dict): if attr.eager: eager_fields.add(name) - class_dict['_eager_fields'] = eager_fields - class_dict['_fields'] = fields | eager_fields + class_dict["_eager_fields"] = eager_fields + class_dict["_fields"] = fields | eager_fields # Call super and continue class creation rv = type.__new__(cls, classname, bases, class_dict) @@ -265,6 +259,7 @@ def wrapper(*args, **kwargs): map_fn = query.instance_class for record in function(*args, **kwargs): yield map_fn(record) if map_fn else record + return wrapper @@ -279,10 +274,11 @@ def wrapper(*args, **kwargs): return query.instance_class(**result) else: return result + return wrapper -class classproperty(object): # NOQA +class classproperty(object): # NOQA def __init__(self, f): self.f = f @@ -308,8 +304,7 @@ def __init__(self, model, instance_class=None): @property def fields(self): - return self.instance_class and tuple(self.instance_class._fields) or \ - None + return self.instance_class and tuple(self.instance_class._fields) or None def __copy__(self): """ @@ -335,7 +330,7 @@ def __copy__(self): def context(self): "Return the context to execute the query" return { - 'active_test': self.active_only, + "active_test": self.active_only, } def _copy(self): @@ -364,18 +359,14 @@ def all(self): def count(self): "Return a count of rows this Query would return." - return self.rpc_model.search_count( - self.domain, context=self.context - ) + return self.rpc_model.search_count(self.domain, context=self.context) def exists(self): """ A convenience method that returns True if a record satisfying the query exists """ - return self.rpc_model.search_count( - self.domain, context=self.context - ) > 0 + return self.rpc_model.search_count(self.domain, context=self.context) > 0 def show_active_only(self, state): """ @@ -392,9 +383,7 @@ def filter_by(self, **kwargs): """ query = self._copy() for field, value in kwargs.items(): - query.domain.append( - (field, '=', value) - ) + query.domain.append((field, "=", value)) return query def filter_by_domain(self, domain): @@ -412,8 +401,7 @@ def first(self): doesn't contain any row. """ results = self.rpc_model.search_read( - self.domain, None, 1, self._order_by, self.fields, - context=self.context + self.domain, None, 1, self._order_by, self.fields, context=self.context ) return results and results[0] or None @@ -426,11 +414,9 @@ def get(self, id): This returns a record whether active or not. """ ctx = self.context.copy() - ctx['active_test'] = False + ctx["active_test"] = False results = self.rpc_model.search_read( - [('id', '=', id)], - None, None, None, self.fields, - context=ctx + [("id", "=", id)], None, None, None, self.fields, context=ctx ) return results and results[0] or None @@ -460,8 +446,7 @@ def one(self): found. """ results = self.rpc_model.search_read( - self.domain, 2, None, self._order_by, self.fields, - context=self.context + self.domain, 2, None, self._order_by, self.fields, context=self.context ) if not results: raise fulfil_client.exc.NoResultFound @@ -509,7 +494,7 @@ def archive(self): """ ids = self.rpc_model.search(self.domain, context=self.context) if ids: - self.rpc_model.write(ids, {'active': False}) + self.rpc_model.write(ids, {"active": False}) @six.add_metaclass(NamedDescriptorResolverMetaClass) @@ -531,7 +516,7 @@ def __init__(self, values=None, id=None, **kwargs): values.update(kwargs) if id is not None: - values['id'] = id + values["id"] = id # Now create a modification tracking dictionary self._values = ModificationTrackingDict(values) @@ -539,11 +524,7 @@ def __init__(self, values=None, id=None, **kwargs): @classmethod def get_cache_key(cls, id): "Return a cache key for the given id" - return '%s:%s:%s' % ( - cls.fulfil_client.subdomain, - cls.__model_name__, - id - ) + return "%s:%s:%s" % (cls.fulfil_client.subdomain, cls.__model_name__, id) @property def cache_key(self): @@ -579,15 +560,13 @@ def from_cache_multi(cls, ids, ignore_misses=False): misses.append(id) if misses: - cache_logger.warn( - "MISS::MULTI::%s::%s" % (cls.__model_name__, misses) - ) + cache_logger.warn("MISS::MULTI::%s::%s" % (cls.__model_name__, misses)) if misses and not ignore_misses: # Get the records in bulk for misses rows = cls.rpc.read(misses, tuple(cls._fields)) for row in rows: - record = cls(id=row['id'], values=row) + record = cls(id=row["id"], values=row) record.store_in_cache() results.append(record) @@ -642,13 +621,15 @@ def changes(self): """ Return a set of changes """ - return dict([ - (field_name, self._values[field_name]) - for field_name in self._values.changes - ]) + return dict( + [ + (field_name, self._values[field_name]) + for field_name in self._values.changes + ] + ) @classproperty - def query(cls): # NOQA + def query(cls): # NOQA return Query(cls.get_rpc_model(), cls) @property @@ -657,7 +638,7 @@ def has_changed(self): return len(self._values) > 0 @classproperty - def rpc(cls): # NOQA + def rpc(cls): # NOQA "Returns an RPC client for the Fulfil.IO model with same name" return cls.get_rpc_model() @@ -726,21 +707,21 @@ def __eq__(self, other): @property def __url__(self): "Return the API URL for the record" - return '/'.join([ - self.rpc.client.base_url, - self.__model_name__, - six.text_type(self.id) - ]) + return "/".join( + [self.rpc.client.base_url, self.__model_name__, six.text_type(self.id)] + ) @property def __client_url__(self): "Return the Client URL for the record" - return '/'.join([ - self.rpc.client.host, - 'client/#/model', - self.__model_name__, - six.text_type(self.id) - ]) + return "/".join( + [ + self.rpc.client.host, + "client/#/model", + self.__model_name__, + six.text_type(self.id), + ] + ) def model_base(fulfil_client, cache_backend=None, cache_expire=10 * 60): @@ -751,13 +732,13 @@ def model_base(fulfil_client, cache_backend=None, cache_expire=10 * 60): This design is inspired by the declarative base pattern in SQL Alchemy. """ return type( - 'BaseModel', + "BaseModel", (Model,), { - 'fulfil_client': fulfil_client, - 'cache_backend': cache_backend, - 'cache_expire': cache_expire, - '__abstract__': True, - '__modelregistry__': {}, + "fulfil_client": fulfil_client, + "cache_backend": cache_backend, + "cache_expire": cache_expire, + "__abstract__": True, + "__modelregistry__": {}, }, ) diff --git a/fulfil_client/oauth.py b/fulfil_client/oauth.py index 4d1955d..e13a481 100644 --- a/fulfil_client/oauth.py +++ b/fulfil_client/oauth.py @@ -2,7 +2,6 @@ class Session(OAuth2Session): - client_id = None client_secret = None @@ -11,31 +10,27 @@ def __init__(self, subdomain, **kwargs): client_secret = self.client_secret self.fulfil_subdomain = subdomain if not (client_id and client_secret): - raise Exception('Missing client_id or client_secret.') + raise Exception("Missing client_id or client_secret.") super(Session, self).__init__(client_id=client_id, **kwargs) @classmethod def setup(cls, client_id, client_secret): - """Configure client in session - """ + """Configure client in session""" cls.client_id = client_id cls.client_secret = client_secret @property def base_url(self): - if self.fulfil_subdomain == 'localhost': - return 'http://localhost:8000/' + if self.fulfil_subdomain == "localhost": + return "http://localhost:8000/" else: - return 'https://%s.fulfil.io/' % self.fulfil_subdomain + return "https://%s.fulfil.io/" % self.fulfil_subdomain def create_authorization_url(self, redirect_uri, scope, **kwargs): self.redirect_uri = redirect_uri self.scope = scope - return self.authorization_url( - self.base_url + 'oauth/authorize', **kwargs) + return self.authorization_url(self.base_url + "oauth/authorize", **kwargs) def get_token(self, code): - token_url = self.base_url + 'oauth/token' - return self.fetch_token( - token_url, client_secret=self.client_secret, code=code - ) + token_url = self.base_url + "oauth/token" + return self.fetch_token(token_url, client_secret=self.client_secret, code=code) diff --git a/fulfil_client/serialization.py b/fulfil_client/serialization.py index 1e412c5..c2e2939 100644 --- a/fulfil_client/serialization.py +++ b/fulfil_client/serialization.py @@ -16,7 +16,6 @@ class JSONDecoder(object): - decoders = {} @classmethod diff --git a/fulfil_client/signals.py b/fulfil_client/signals.py index dc4eeae..7708ddd 100644 --- a/fulfil_client/signals.py +++ b/fulfil_client/signals.py @@ -1,23 +1,26 @@ # -*- coding: utf-8 -*- """ - flask.signals - ~~~~~~~~~~~~~ - Implements signals based on blinker if available, otherwise - falls silently back to a noop. +flask.signals +~~~~~~~~~~~~~ +Implements signals based on blinker if available, otherwise +falls silently back to a noop. - :copyright: (c) 2018 Fulfil.IO Inc. +:copyright: (c) 2018 Fulfil.IO Inc. - The blinker fallback code is inspired by Armin's implementation - on Flask. - :copyright: (c) 2015 by Armin Ronacher. +The blinker fallback code is inspired by Armin's implementation +on Flask. +:copyright: (c) 2015 by Armin Ronacher. - :license: BSD, see LICENSE for more details. +:license: BSD, see LICENSE for more details. """ + signals_available = False try: from blinker import Namespace + signals_available = True except ImportError: + class Namespace(object): def signal(self, name, doc=None): return _FakeSignal(name, doc) @@ -32,17 +35,22 @@ class _FakeSignal(object): def __init__(self, name, doc=None): self.name = name self.__doc__ = doc + def _fail(self, *args, **kwargs): - raise RuntimeError('signalling support is unavailable ' - 'because the blinker library is ' - 'not installed.') + raise RuntimeError( + "signalling support is unavailable " + "because the blinker library is " + "not installed." + ) + send = lambda *a, **kw: None - connect = disconnect = has_receivers_for = receivers_for = \ - temporarily_connected_to = connected_to = _fail + connect = disconnect = has_receivers_for = receivers_for = ( + temporarily_connected_to + ) = connected_to = _fail del _fail # Namespace for signals _signals = Namespace() -response_received = _signals.signal('response-received') +response_received = _signals.signal("response-received") diff --git a/setup.py b/setup.py index 38e2f23..f22ed57 100755 --- a/setup.py +++ b/setup.py @@ -8,58 +8,58 @@ from distutils.core import setup -with open('README.rst') as readme_file: +with open("README.rst") as readme_file: readme = readme_file.read() -with open('HISTORY.rst') as history_file: +with open("HISTORY.rst") as history_file: history = history_file.read() requirements = [ - 'pyjwt', - 'requests', - 'requests_oauthlib', - 'money', - 'babel', - 'six', - 'more-itertools', - 'isodate', + "pyjwt", + "requests", + "requests_oauthlib", + "money", + "babel", + "six", + "more-itertools", + "isodate", ] setup( - name='fulfil_client', - version='2.0.0', + name="fulfil_client", + version="2.0.0", description="Fulfil REST API Client in Python", - long_description=readme + '\n\n' + history, + long_description=readme + "\n\n" + history, author="Fulfil.IO Inc.", - author_email='hello@fulfil.io', - url='https://github.com/fulfilio/fulfil-python-api', + author_email="hello@fulfil.io", + url="https://github.com/fulfilio/fulfil-python-api", packages=[ - 'fulfil_client', - 'fulfil_client.contrib', + "fulfil_client", + "fulfil_client.contrib", ], package_dir={ - 'fulfil_client': 'fulfil_client', - 'fulfil_client.contrib': 'fulfil_client/contrib' + "fulfil_client": "fulfil_client", + "fulfil_client.contrib": "fulfil_client/contrib", }, entry_points={ - 'kombu.serializers': [ - 'fulfil = fulfil_client.contrib.kombu:register_args', + "kombu.serializers": [ + "fulfil = fulfil_client.contrib.kombu:register_args", ], }, include_package_data=True, install_requires=requirements, license="ISCL", zip_safe=False, - keywords='fulfil_client', + keywords="fulfil_client", classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: ISC License (ISCL)', - 'Natural Language :: English', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', + "Development Status :: 2 - Pre-Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: ISC License (ISCL)", + "Natural Language :: English", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", ], - setup_requires=['pytest-runner'], - tests_require=['pytest', 'redis'], + setup_requires=["pytest-runner"], + tests_require=["pytest", "redis"], ) diff --git a/tests/conftest.py b/tests/conftest.py index 8c49e49..0232e3d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Defines fixtures available to all tests.""" + import os import pytest @@ -11,14 +12,12 @@ @pytest.fixture def client(): - return Client('demo', os.environ['FULFIL_API_KEY']) + return Client("demo", os.environ["FULFIL_API_KEY"]) @pytest.fixture def oauth_client(): - return Client( - 'demo', auth=BearerAuth(os.environ['FULFIL_OAUTH_TOKEN']) - ) + return Client("demo", auth=BearerAuth(os.environ["FULFIL_OAUTH_TOKEN"])) @pytest.fixture @@ -29,6 +28,5 @@ def Model(client): @pytest.fixture def ModelWithCache(client): return model_base( - client, - cache_backend=redis.StrictRedis(host='localhost', port=6379, db=0) + client, cache_backend=redis.StrictRedis(host="localhost", port=6379, db=0) ) diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py index b074f78..df4827f 100644 --- a/tests/test_data_structures.py +++ b/tests/test_data_structures.py @@ -5,6 +5,7 @@ pylint option block-disable """ + import pickle import pytest import random @@ -12,11 +13,12 @@ from babel.numbers import format_currency from money import Money -from fulfil_client.model import ( - ModificationTrackingDict, Query, StringType, MoneyType -) +from fulfil_client.model import ModificationTrackingDict, Query, StringType, MoneyType from fulfil_client.exceptions import ( - ServerError, ClientError, AuthenticationError, UserError + ServerError, + ClientError, + AuthenticationError, + UserError, ) @@ -25,51 +27,51 @@ def mtd(): """ Return a sample Modification Tracking Dictionary """ - return ModificationTrackingDict({ - 'a': 'apple', - 'b': 'box', - 'l': [1, 2, 3], - }) + return ModificationTrackingDict( + { + "a": "apple", + "b": "box", + "l": [1, 2, 3], + } + ) class TestModificationTrackingDict(object): - def test_no_changes_on_initial_dict(self, mtd): assert len(mtd.changes) == 0 def test_no_changes_on_same_value(self, mtd): - mtd['a'] = 'apple' # nothing changes + mtd["a"] = "apple" # nothing changes assert len(mtd.changes) == 0 def test_no_changes_on_same_value_on_update(self, mtd): - mtd.update({'a': 'apple'}) + mtd.update({"a": "apple"}) assert len(mtd.changes) == 0 def test_changes_on_setter(self, mtd): - mtd['b'] = 'ball' # big change + mtd["b"] = "ball" # big change assert len(mtd.changes) == 1 - assert 'b' in mtd.changes + assert "b" in mtd.changes def test_changes_on_update(self, mtd): - mtd.update({'b': 'ball'}) # big change + mtd.update({"b": "ball"}) # big change assert len(mtd.changes) == 1 - assert 'b' in mtd.changes + assert "b" in mtd.changes def test_changes_on_new_key(self, mtd): - mtd['c'] = 'cat' + mtd["c"] = "cat" assert len(mtd.changes) == 1 - assert 'c' in mtd.changes + assert "c" in mtd.changes @pytest.fixture def query(client): return Query( - client.model('res.user'), + client.model("res.user"), ) class TestQuery(object): - def test_copyability_of_query(self, query): query._copy() @@ -86,31 +88,33 @@ def test_query_all(self, query): @pytest.fixture def res_user_model(Model): class ResUserModel(Model): - __model_name__ = 'res.user' + __model_name__ = "res.user" name = StringType() + return ResUserModel @pytest.fixture def res_user_model_with_cache(ModelWithCache): class ResUserModel(ModelWithCache): - __model_name__ = 'res.user' + __model_name__ = "res.user" name = StringType() + return ResUserModel @pytest.fixture def sale_order_model(Model): class SaleOrderModel(Model): - __model_name__ = 'sale.sale' - _eager_fields = set(['currency.code']) + __model_name__ = "sale.sale" + _eager_fields = set(["currency.code"]) number = StringType() - total_amount = MoneyType('currency_code') + total_amount = MoneyType("currency_code") @property def currency_code(self): - return self._values['currency.code'] + return self._values["currency.code"] return SaleOrderModel @@ -118,13 +122,13 @@ def currency_code(self): @pytest.fixture def product_model(Model): class ProductModel(Model): - __model_name__ = 'product.product' + __model_name__ = "product.product" - list_price = MoneyType('currency_code') + list_price = MoneyType("currency_code") @property def currency_code(self): - return 'USD' + return "USD" return ProductModel @@ -132,34 +136,35 @@ def currency_code(self): @pytest.fixture def contact_model(Model): class ContactModel(Model): - __model_name__ = 'party.party' + __model_name__ = "party.party" name = StringType() - credit_limit_amount = MoneyType('currency_code') + credit_limit_amount = MoneyType("currency_code") @property def currency_code(self): - return 'USD' + return "USD" + return ContactModel @pytest.fixture def module_model(Model): class ModuleModel(Model): - __model_name__ = 'ir.module' + __model_name__ = "ir.module" name = StringType() + return ModuleModel class TestModel(object): - def test_model_change_tracking(self, res_user_model): user = res_user_model.query.first() user.name = user.name assert not bool(user.changes) user.name = "Not real name" - assert 'name' in user.changes + assert "name" in user.changes def test_equality_of_saved_records(self, res_user_model): user = res_user_model.query.first() @@ -196,20 +201,19 @@ def test_api_url(self, res_user_model): class TestMoneyType(object): - def test_display_format(self, sale_order_model): order = sale_order_model.query.first() assert isinstance(order.total_amount, Money) assert isinstance(order.total_amount.amount, Decimal) - assert order.total_amount.format('en_US') == format_currency( - order._values['total_amount'], - currency=order._values['currency.code'], - locale='en_US' + assert order.total_amount.format("en_US") == format_currency( + order._values["total_amount"], + currency=order._values["currency.code"], + locale="en_US", ) - assert order.total_amount.format('fr_FR') == format_currency( - order._values['total_amount'], - currency=order._values['currency.code'], - locale='fr_FR' + assert order.total_amount.format("fr_FR") == format_currency( + order._values["total_amount"], + currency=order._values["currency.code"], + locale="fr_FR", ) def test_setting_values(self, product_model): @@ -221,10 +225,9 @@ def test_setting_values(self, product_model): list_price = product_model.query.first().list_price assert list_price.amount == new_price - assert list_price.currency == 'USD' # hard coded in model property + assert list_price.currency == "USD" # hard coded in model property def test_none(self, contact_model): - contact = contact_model.query.first() contact.credit_limit_amount = None @@ -233,18 +236,17 @@ def test_none(self, contact_model): credit_limit = contact.query.first().credit_limit_amount assert credit_limit is None - contact.credit_limit_amount = Decimal('100000') + contact.credit_limit_amount = Decimal("100000") contact.save() credit_limit = contact.query.first().credit_limit_amount - assert credit_limit.amount == Decimal('100000') - assert credit_limit.currency == 'USD' # hard coded in model property + assert credit_limit.amount == Decimal("100000") + assert credit_limit.currency == "USD" # hard coded in model property -@pytest.mark.parametrize("error_class", [ - ServerError, ClientError, AuthenticationError, - UserError -]) +@pytest.mark.parametrize( + "error_class", [ServerError, ClientError, AuthenticationError, UserError] +) def test_exception_pickling(error_class): "Test that exceptions can be pickled" error = error_class("Shit Happens", "123") diff --git a/tests/test_fulfil_client.py b/tests/test_fulfil_client.py index aa90552..a18bf15 100755 --- a/tests/test_fulfil_client.py +++ b/tests/test_fulfil_client.py @@ -1,141 +1,118 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - """ test_fulfil_client ---------------------------------- Tests for `fulfil_client` module. """ + import pytest from fulfil_client import Client, ClientError, ServerError def test_find(client): - IRModel = client.model('ir.model') + IRModel = client.model("ir.model") ir_models = IRModel.find([]) assert len(ir_models) > 0 - assert ir_models[0]['id'] - assert ir_models[0]['rec_name'] + assert ir_models[0]["id"] + assert ir_models[0]["rec_name"] def test_search_read_all(client): - IRView = client.model('ir.ui.view') + IRView = client.model("ir.ui.view") total_records = IRView.search_count([]) - ir_models = list( - IRView.search_read_all([], None, ['rec_name'], batch_size=50) - ) + ir_models = list(IRView.search_read_all([], None, ["rec_name"], batch_size=50)) # the default batch size is 500 and the total records # being greater than that is an important part of the test. assert total_records > 500 - assert total_records == len(set([r['id'] for r in ir_models])) + assert total_records == len(set([r["id"] for r in ir_models])) assert len(ir_models) == total_records assert len(ir_models) > 10 - assert ir_models[0]['id'] - assert ir_models[0]['rec_name'] + assert ir_models[0]["id"] + assert ir_models[0]["rec_name"] first_record = ir_models[0] # Offset and then fetch - ir_models = list( - IRView.search_read_all( - [], None, ['rec_name'], offset=10 - ) - ) + ir_models = list(IRView.search_read_all([], None, ["rec_name"], offset=10)) assert len(ir_models) == total_records - 10 - assert ir_models[0]['id'] != first_record['id'] + assert ir_models[0]["id"] != first_record["id"] # Smaller batch size and offset ir_models = list( - IRView.search_read_all( - [], None, ['rec_name'], batch_size=5, offset=10 - ) + IRView.search_read_all([], None, ["rec_name"], batch_size=5, offset=10) ) assert len(ir_models) == total_records - 10 - assert ir_models[0]['id'] != first_record['id'] + assert ir_models[0]["id"] != first_record["id"] # Smaller batch size and limit ir_models = list( - IRView.search_read_all( - [], None, ['rec_name'], batch_size=5, limit=10 - ) + IRView.search_read_all([], None, ["rec_name"], batch_size=5, limit=10) ) assert len(ir_models) == 10 - assert ir_models[0]['id'] == first_record['id'] + assert ir_models[0]["id"] == first_record["id"] # default batch size and limit - ir_models = list( - IRView.search_read_all( - [], None, ['rec_name'], limit=10 - ) - ) + ir_models = list(IRView.search_read_all([], None, ["rec_name"], limit=10)) assert len(ir_models) == 10 - assert ir_models[0]['id'] == first_record['id'] + assert ir_models[0]["id"] == first_record["id"] # small batch size and limit and offset ir_models = list( - IRView.search_read_all( - [], None, ['rec_name'], - batch_size=5, limit=10, offset=5 - ) + IRView.search_read_all([], None, ["rec_name"], batch_size=5, limit=10, offset=5) ) assert len(ir_models) == 10 - assert ir_models[0]['id'] != first_record['id'] + assert ir_models[0]["id"] != first_record["id"] # default batch size and limit and offset - ir_models = list( - IRView.search_read_all( - [], None, ['rec_name'], - limit=10, offset=5 - ) - ) + ir_models = list(IRView.search_read_all([], None, ["rec_name"], limit=10, offset=5)) assert len(ir_models) == 10 - assert ir_models[0]['id'] != first_record['id'] + assert ir_models[0]["id"] != first_record["id"] def test_find_no_filter(client): - IRModel = client.model('ir.model') + IRModel = client.model("ir.model") ir_models = IRModel.find() assert len(ir_models) > 0 - assert ir_models[0]['id'] - assert ir_models[0]['rec_name'] + assert ir_models[0]["id"] + assert ir_models[0]["rec_name"] def test_raises_server_error(client): - Model = client.model('ir.model') + Model = client.model("ir.model") with pytest.raises(ServerError): Model.search(1) def test_raises_client_error(): with pytest.raises(ClientError): - Client('demo', 'wrong-api-key') + Client("demo", "wrong-api-key") def test_wizard_implementation(oauth_client): - DuplicateWizard = oauth_client.wizard('ir.model.duplicate') - Party = oauth_client.model('party.party') + DuplicateWizard = oauth_client.wizard("ir.model.duplicate") + Party = oauth_client.model("party.party") existing_parties = Party.search([], None, 1, None) if not existing_parties: pytest.fail("No existing parties to duplicate") - existing_party, = existing_parties + (existing_party,) = existing_parties with DuplicateWizard.session( - active_ids=[existing_party], active_id=existing_party, - active_model='party.party' + active_ids=[existing_party], + active_id=existing_party, + active_model="party.party", ) as wizard: - result = wizard.execute('duplicate_records') - assert 'actions' in result - action, data = result['actions'][0] - assert 'res_id' in data - assert len(data['res_id']) == 1 - Party.delete(data['res_id']) + result = wizard.execute("duplicate_records") + assert "actions" in result + action, data = result["actions"][0] + assert "res_id" in data + assert len(data["res_id"]) == 1 + Party.delete(data["res_id"]) def test_403(): "Connect with invalid creds and get ClientError" with pytest.raises(ClientError): - Client('demo', 'xxxx') + Client("demo", "xxxx") diff --git a/tests/test_mocking.py b/tests/test_mocking.py index 6997520..aa3b127 100644 --- a/tests/test_mocking.py +++ b/tests/test_mocking.py @@ -1,28 +1,23 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- import pytest import fulfil_client from fulfil_client.contrib.mocking import MockFulfil def api_calling_method(): - client = fulfil_client.Client('apple', 'apples-api-key') - Product = client.model('product.product') - products = Product.search_read_all([], None, ['id']) - Product.write( - [p['id'] for p in products], - {'active': False} - ) + client = fulfil_client.Client("apple", "apples-api-key") + Product = client.model("product.product") + products = Product.search_read_all([], None, ["id"]) + Product.write([p["id"] for p in products], {"active": False}) return client def test_mock_1(): - with MockFulfil('fulfil_client.Client') as mocked_fulfil: - Product = mocked_fulfil.model('product.product') + with MockFulfil("fulfil_client.Client") as mocked_fulfil: + Product = mocked_fulfil.model("product.product") Product.search_read_all.return_value = [ - {'id': 1}, - {'id': 2}, - {'id': 3}, + {"id": 1}, + {"id": 2}, + {"id": 3}, ] # Call the function @@ -30,30 +25,29 @@ def test_mock_1(): # Now assert Product.search_read_all.assert_called() - Product.search_read_all.assert_called_with([], None, ['id']) - Product.write.assert_called_with( - [1, 2, 3], {'active': False} - ) + Product.search_read_all.assert_called_with([], None, ["id"]) + Product.write.assert_called_with([1, 2, 3], {"active": False}) def test_mock_context(): "Ensure that old mocks die with the context" - with MockFulfil('fulfil_client.Client') as mocked_fulfil: - Product = mocked_fulfil.model('product.product') + with MockFulfil("fulfil_client.Client") as mocked_fulfil: + Product = mocked_fulfil.model("product.product") api_calling_method() Product.search_read_all.assert_called() # Start new context - with MockFulfil('fulfil_client.Client') as mocked_fulfil: - Product = mocked_fulfil.model('product.product') + with MockFulfil("fulfil_client.Client") as mocked_fulfil: + Product = mocked_fulfil.model("product.product") Product.search_read_all.assert_not_called() def test_mock_different_return_vals(): "Return different values based on mock side_effect" + def lookup_products(domain): - client = fulfil_client.Client('apple', 'apples-api-key') - Product = client.model('product.product') + client = fulfil_client.Client("apple", "apples-api-key") + Product = client.model("product.product") return Product.search(domain) def fake_search(domain): @@ -61,11 +55,11 @@ def fake_search(domain): # domain. if domain == []: return [1, 2, 3, 4, 5] - elif domain == [('salable', '=', True)]: + elif domain == [("salable", "=", True)]: return [1, 2, 3] - with MockFulfil('fulfil_client.Client') as mocked_fulfil: - Product = mocked_fulfil.model('product.product') + with MockFulfil("fulfil_client.Client") as mocked_fulfil: + Product = mocked_fulfil.model("product.product") Product.search.side_effect = fake_search assert lookup_products([]) == [1, 2, 3, 4, 5] - assert lookup_products([('salable', '=', True)]) == [1, 2, 3] + assert lookup_products([("salable", "=", True)]) == [1, 2, 3] diff --git a/travis_pypi_setup.py b/travis_pypi_setup.py deleted file mode 100755 index 5ba3a30..0000000 --- a/travis_pypi_setup.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -"""Update encrypted deploy password in Travis config file -""" - - -from __future__ import print_function -import base64 -import json -import os -from getpass import getpass -import yaml -from cryptography.hazmat.primitives.serialization import load_pem_public_key -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15 - - -try: - from urllib import urlopen -except: - from urllib.request import urlopen - - -GITHUB_REPO = 'fulfilio/fulfil_client' -TRAVIS_CONFIG_FILE = os.path.join( - os.path.dirname(os.path.abspath(__file__)), '.travis.yml') - - -def load_key(pubkey): - """Load public RSA key, with work-around for keys using - incorrect header/footer format. - - Read more about RSA encryption with cryptography: - https://cryptography.io/latest/hazmat/primitives/asymmetric/rsa/ - """ - try: - return load_pem_public_key(pubkey.encode(), default_backend()) - except ValueError: - # workaround for https://github.com/travis-ci/travis-api/issues/196 - pubkey = pubkey.replace('BEGIN RSA', 'BEGIN').replace('END RSA', 'END') - return load_pem_public_key(pubkey.encode(), default_backend()) - - -def encrypt(pubkey, password): - """Encrypt password using given RSA public key and encode it with base64. - - The encrypted password can only be decrypted by someone with the - private key (in this case, only Travis). - """ - key = load_key(pubkey) - encrypted_password = key.encrypt(password, PKCS1v15()) - return base64.b64encode(encrypted_password) - - -def fetch_public_key(repo): - """Download RSA public key Travis will use for this repo. - - Travis API docs: http://docs.travis-ci.com/api/#repository-keys - """ - keyurl = 'https://api.travis-ci.org/repos/{0}/key'.format(repo) - data = json.loads(urlopen(keyurl).read().decode()) - if 'key' not in data: - errmsg = "Could not find public key for repo: {}.\n".format(repo) - errmsg += "Have you already added your GitHub repo to Travis?" - raise ValueError(errmsg) - return data['key'] - - -def prepend_line(filepath, line): - """Rewrite a file adding a line to its beginning. - """ - with open(filepath) as f: - lines = f.readlines() - - lines.insert(0, line) - - with open(filepath, 'w') as f: - f.writelines(lines) - - -def load_yaml_config(filepath): - with open(filepath) as f: - return yaml.load(f) - - -def save_yaml_config(filepath, config): - with open(filepath, 'w') as f: - yaml.dump(config, f, default_flow_style=False) - - -def update_travis_deploy_password(encrypted_password): - """Update the deploy section of the .travis.yml file - to use the given encrypted password. - """ - config = load_yaml_config(TRAVIS_CONFIG_FILE) - - config['deploy']['password'] = dict(secure=encrypted_password) - - save_yaml_config(TRAVIS_CONFIG_FILE, config) - - line = ('# This file was autogenerated and will overwrite' - ' each time you run travis_pypi_setup.py\n') - prepend_line(TRAVIS_CONFIG_FILE, line) - - -def main(args): - public_key = fetch_public_key(args.repo) - password = args.password or getpass('PyPI password: ') - update_travis_deploy_password(encrypt(public_key, password.encode())) - print("Wrote encrypted password to .travis.yml -- you're ready to deploy") - - -if '__main__' == __name__: - import argparse - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--repo', default=GITHUB_REPO, - help='GitHub repo (default: %s)' % GITHUB_REPO) - parser.add_argument('--password', - help='PyPI password (will prompt if not provided)') - - args = parser.parse_args() - main(args) From 646fc142cc18cee6c0ffe30e96cd44c0b2176c1a Mon Sep 17 00:00:00 2001 From: Sharoon Thomas Date: Tue, 7 May 2024 17:29:10 +0200 Subject: [PATCH 4/6] Remove docs folder --- docs/Makefile | 177 --------------------------- docs/authors.rst | 1 - docs/conf.py | 278 ------------------------------------------ docs/contributing.rst | 1 - docs/history.rst | 1 - docs/index.rst | 27 ---- docs/installation.rst | 14 --- docs/make.bat | 242 ------------------------------------ docs/readme.rst | 1 - docs/usage.rst | 7 -- 10 files changed, 749 deletions(-) delete mode 100644 docs/Makefile delete mode 100644 docs/authors.rst delete mode 100755 docs/conf.py delete mode 100644 docs/contributing.rst delete mode 100644 docs/history.rst delete mode 100644 docs/index.rst delete mode 100644 docs/installation.rst delete mode 100644 docs/make.bat delete mode 100644 docs/readme.rst delete mode 100644 docs/usage.rst diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 6f263d3..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,177 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/fulfil_client.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/fulfil_client.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/fulfil_client" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/fulfil_client" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/docs/authors.rst b/docs/authors.rst deleted file mode 100644 index e122f91..0000000 --- a/docs/authors.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../AUTHORS.rst diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100755 index 9918554..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# fulfil_client documentation build configuration file, created by -# sphinx-quickstart on Tue Jul 9 22:26:36 2013. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os - -# If extensions (or modules to document with autodoc) are in another -# directory, add these directories to sys.path here. If the directory is -# relative to the documentation root, use os.path.abspath to make it -# absolute, like shown here. -# sys.path.insert(0, os.path.abspath('.')) - -# Get the project root dir, which is the parent dir of this -cwd = os.getcwd() -project_root = os.path.dirname(cwd) - -# Insert the project root dir as the first element in the PYTHONPATH. -# This lets us ensure that the source package is imported, and that its -# version is used. -sys.path.insert(0, project_root) - -import fulfil_client - -# -- General configuration --------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ["sphinx.ext.autodoc", "sphinx.ext.viewcode"] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# The suffix of source filenames. -source_suffix = ".rst" - -# The encoding of source files. -# source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = "index" - -# General information about the project. -project = "fulfil_client" -copyright = "2016, Fulfil.IO Inc." - -# The version info for the project you're documenting, acts as replacement -# for |version| and |release|, also used in various other places throughout -# the built documents. -# -# The short X.Y version. -version = fulfil_client.__version__ -# The full version, including alpha/beta/rc tags. -release = fulfil_client.__version__ - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to -# some non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ["_build"] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built -# documents. -# keep_warnings = False - - -# -- Options for HTML output ------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = "default" - -# Theme options are theme-specific and customize the look and feel of a -# theme further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as -# html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the -# top of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon -# of the docs. This file should be a Windows icon file (.ico) being -# 16x16 or 32x32 pixels large. -# html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) -# here, relative to this directory. They are copied after the builtin -# static files, so a file named "default.css" will overwrite the builtin -# "default.css". -html_static_path = ["_static"] - -# If not '', a 'Last updated on:' timestamp is inserted at every page -# bottom, using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names -# to template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. -# Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. -# Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages -# will contain a tag referring to it. The value of this option -# must be the base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = "fulfil_clientdoc" - - -# -- Options for LaTeX output ------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - #'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - #'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - #'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass -# [howto/manual]). -latex_documents = [ - ( - "index", - "fulfil_client.tex", - "fulfil_client Documentation", - "Fulfil.IO Inc.", - "manual", - ), -] - -# The name of an image file (relative to this directory) to place at -# the top of the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings -# are parts, not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output ------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ("index", "fulfil_client", "fulfil_client Documentation", ["Fulfil.IO Inc."], 1) -] - -# If true, show URL addresses after external links. -# man_show_urls = False - - -# -- Options for Texinfo output ---------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - "index", - "fulfil_client", - "fulfil_client Documentation", - "Fulfil.IO Inc.", - "fulfil_client", - "One line description of project.", - "Miscellaneous", - ), -] - -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -# texinfo_no_detailmenu = False diff --git a/docs/contributing.rst b/docs/contributing.rst deleted file mode 100644 index e582053..0000000 --- a/docs/contributing.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../CONTRIBUTING.rst diff --git a/docs/history.rst b/docs/history.rst deleted file mode 100644 index 2506499..0000000 --- a/docs/history.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../HISTORY.rst diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index f34804f..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,27 +0,0 @@ -.. fulfil_client documentation master file, created by - sphinx-quickstart on Tue Jul 9 22:26:36 2013. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to fulfil_client's documentation! -====================================== - -Contents: - -.. toctree:: - :maxdepth: 2 - - readme - installation - usage - contributing - authors - history - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - diff --git a/docs/installation.rst b/docs/installation.rst deleted file mode 100644 index 9807552..0000000 --- a/docs/installation.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. highlight:: shell - -============ -Installation -============ - -At the command line:: - - $ easy_install fulfil_client - -Or, if you have virtualenvwrapper installed:: - - $ mkvirtualenv fulfil_client - $ pip install fulfil_client diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 0559276..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,242 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\fulfil_client.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\fulfil_client.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end diff --git a/docs/readme.rst b/docs/readme.rst deleted file mode 100644 index 72a3355..0000000 --- a/docs/readme.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../README.rst diff --git a/docs/usage.rst b/docs/usage.rst deleted file mode 100644 index 622f2a5..0000000 --- a/docs/usage.rst +++ /dev/null @@ -1,7 +0,0 @@ -===== -Usage -===== - -To use fulfil_client in a project:: - - import fulfil_client From b39bffdb71807ec58d18398c10ae73a5a25a2016 Mon Sep 17 00:00:00 2001 From: Sharoon Thomas Date: Tue, 7 May 2024 17:29:31 +0200 Subject: [PATCH 5/6] Ruff fixes --- tests/test_mocking.py | 1 - tests/test_serialization.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/tests/test_mocking.py b/tests/test_mocking.py index aa3b127..309987a 100644 --- a/tests/test_mocking.py +++ b/tests/test_mocking.py @@ -1,4 +1,3 @@ -import pytest import fulfil_client from fulfil_client.contrib.mocking import MockFulfil diff --git a/tests/test_serialization.py b/tests/test_serialization.py index 7b07fbd..350d644 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -5,8 +5,6 @@ from fulfil_client.serialization import ( dumps, loads, - JSONDecoder, - JSONEncoder, ) import pytest From 69f26773f5644c394f63054619599799481e1070 Mon Sep 17 00:00:00 2001 From: Sharoon Thomas Date: Tue, 7 May 2024 17:35:44 +0200 Subject: [PATCH 6/6] Clean up CI file --- .github/workflows/ci.yml | 36 +++++++++++++---------------------- .pre-commit-config.yaml | 10 ++++++++++ fulfil_client/signals.py | 3 +-- tests/test_data_structures.py | 2 +- 4 files changed, 25 insertions(+), 26 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d11c8a0..c9043b5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: chartboost/ruff-action@v1 + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Python Lint + run: | + pip install ruff>=0.4.3 + ruff check . + ruff format --check . build: strategy: matrix: @@ -16,34 +23,17 @@ jobs: - name: Checkout uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements_dev.txt + - name: Install from source (required for the pre-commit tests) run: pip install . + - name: Test with pytest - run: pytest --cov=./ --cov-report=xml tests/test_serialization.py - release: - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Set up Python 3.11 - uses: actions/setup-python@v1 - with: - python-version: 3.11 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements_dev.txt - make dist - - name: Publish package - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - uses: pypa/gh-action-pypi-publish@master - with: - user: __token__ - password: ${{ secrets.pypi_password }} + run: pytest tests/test_serialization.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..8734090 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.4.3 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/fulfil_client/signals.py b/fulfil_client/signals.py index 7708ddd..c608e1c 100644 --- a/fulfil_client/signals.py +++ b/fulfil_client/signals.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ flask.signals ~~~~~~~~~~~~~ @@ -43,7 +42,7 @@ def _fail(self, *args, **kwargs): "not installed." ) - send = lambda *a, **kw: None + send = lambda *a, **kw: None # noqa connect = disconnect = has_receivers_for = receivers_for = ( temporarily_connected_to ) = connected_to = _fail diff --git a/tests/test_data_structures.py b/tests/test_data_structures.py index df4827f..a378919 100644 --- a/tests/test_data_structures.py +++ b/tests/test_data_structures.py @@ -183,7 +183,7 @@ def test_multi_cache_with_redis(self, res_user_model_with_cache): def test_multi_cache_empty_list(self, res_user_model_with_cache): "Should not raise an error" - records = res_user_model_with_cache.from_cache_multi([]) + res_user_model_with_cache.from_cache_multi([]) def test_inequality_of_saved_records(self, res_user_model, module_model): assert res_user_model.query.first() != module_model.query.first()