├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat ├── modules.rst ├── py3crdt.rst └── tests.rst ├── py3crdt ├── __init__.py ├── gcounter.py ├── gset.py ├── lww.py ├── node.py ├── orset.py ├── pncounter.py ├── sequence.py └── twopset.py ├── setup.py └── tests ├── __init__.py ├── test_gcounter.py ├── test_gset.py ├── test_lww.py ├── test_orset.py ├── test_pncounter.py ├── test_sequence.py └── test_twopset.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [anshulahuja98, geetesh-gupta] 4 | #patreon: # Replace with a single Patreon username 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # IDE 107 | src/.idea 108 | .idea 109 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Geetesh Gupta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | 10 | [requires] 11 | python_version = "3.7" 12 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "7e7ef69da7248742e869378f8421880cf8f0017f96d94d086813baa518a65489" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.7" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": {}, 19 | "develop": {} 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python3-crdt 2 | A python library for CRDTs (Conflict-free Replicated Data types) 3 | 4 | ## Installation 5 | You can get the library directly from PyPI: 6 | 7 | ```python 8 | pip install python3-crdt 9 | ``` 10 | ## Documentation 11 | Checkout the documentation of the library on [ReadtheDocs](https://python3-crdt.readthedocs.io/en/latest/) 12 | Created and Maintained by: 13 | - [Anshul Ahuja](https://github.com/anshulahuja98) 14 | - [Geetesh Gupta](https://github.com/geetesh-gupta) 15 | 16 | ## Usage 17 | If you have installed the python3-crdt package you can start using the crdts right away: 18 | ```python 19 | from py3crdt.gset import GSet 20 | gset1 = GSet(id=1) 21 | gset2 = GSet(id=2) 22 | gset1.add('a') 23 | gset1.add('b') 24 | gset1.display() 25 | # ['a', 'b'] ----- Output 26 | gset2.add('b') 27 | gset2.add('c') 28 | gset2.display() 29 | # ['b', 'c'] ----- Output 30 | gset1.merge(gset2) 31 | gset1.display() 32 | # ['a', 'b', 'c'] ----- Output 33 | gset2.merge(gset1) 34 | gset2.display() 35 | # ['a', 'b', 'c'] ----- Output 36 | ``` 37 | 38 | #### CRDTs deployed:- 39 | - gcounter.GCounter 40 | - pncounter.PNCounter 41 | - gset.GSet 42 | - twopset.TwoPSet 43 | - lww.LWWElementSet 44 | - orest.ORSet 45 | - sequence.Sequence 46 | 47 | ## API 48 | - add() 49 | - remove() 50 | - merge() 51 | - display() 52 | - query() 53 | 54 | ## Testing 55 | Use following command to test packages 56 | ```python 57 | python -m unittest tests.test_ 58 | ``` 59 | ## Intro to CRDTs 60 | #### What are CRDTS? 61 | CRDTs or Conflict-Free Replicated Data Types are data structures which eases the replication of data across multiple devices in a network. Any change/update is applied locally and then transmitted to other replicas. Each replica merges it’s local replica with the incoming change/update. Inconsistencies might arise during merging but CRDTs mathematically guarantees that the replicas will converge eventually if all the changes/updates are executed by each replica. 62 | 63 | #### Types of CRDTs 64 | 65 | ##### Operation-based CRDTs 66 | In these CRDTs, change/update operations are transmitted to other replicas. Each replica receives the operations and apply the operations to its local state. These are also known as CmRDT (Commutative Replicated Data Type) because the operations are commutative hence, order of sending operations does not matter. The resulting state will eventually be the same. But the operations are not idempotent hence, it must be ensured that no operation is duplicated during transmission. 67 | 68 | ##### State-based CRDTs 69 | In these CRDTs, full state is transmitted to other replicas. Replicas receive the state and merge it with the local state. Merge function is commutative as CmRDTs but is also idempotent and associative. These are also known as CvRDT (Convergent Replicated Data Type) because in every transmission merging of states occur, which eventually results in all replicas converging to the same state. 70 | 71 | ##### Delta-state CRDTs 72 | In these CRDTs, instead of full state, only recently applied changes are transmitted to other replicas. It is just an optimised State-based CRDT. 73 | 74 | #### Comparison between CmRDTs & CvRDTs 75 | CmRDTs increases transmission mechanism workload but consumes less bandwidth than CvRDTs when number of transactions is small compared to size of the internal state. However, since the CvRDT merge function is associative merging the state produces all previous updates to that replica and since it is idempotent, the states can be transmitted multiple number of times but resulting into the same state. 76 | 77 | ## CRDTs deployed in this library 78 | 79 | #### G-Counter (Grow-only Counter) 80 | It implements an array of nodes where the value of array works as a counter. The value of array is sum of the values of the nodes in the array. Each node is assigned an ID equivalent to the index of the node in the array. The array is an equivalent for a cluster of nodes. Updating involves each node incrementing its own index value in the array. Merging occurs by taking the maximum of every node value in the cluster. Comparison function is included to verify the increments. Internal state is monotonically increased by application of each update function according to the compare function. 81 | 82 | #### PN-Counter (Positive-Negative Counter) 83 | This counter supports both increment and decrement operations. It combines two G-Counters namely “P” (for incrementing) and “N” (for decrementing) counter. The value of the counter is the value of the P counter minus the value of the N counter. Merging involves merging the P and N counter independently. 84 | 85 | #### G-Set (Grow-only Set) 86 | This involves creating a set of elements where elements can only be added and once and element is added, it cannot be removed. Merging returns union of the two G-Sets. 87 | 88 | #### 2P-Set (Two-Phase Set) 89 | It involves creating a set in which elements can be added as well as removed. Similar to PN-Counter, it combines two G-Sets namely “add” and “remove” set. For adding/removing an element, it is inserted in the “add”/“remove” set. An element is a member of the set if it is in the “add” set but not in the “remove” set. Query function returns whether the element is a member of the set or not. Hence, if an element is removed, query will never return True for that element, so it cannot be re-added. Merging involves union of the “add”/“remove” sets. 90 | 91 | #### LWW-Element-Set (Last-Write-Wins-Element-Set) 92 | Similar to 2P-Set except each element is added/removed with a timestamp. An element is a member of the set if it is in the “add” set but not in the “remove” set, or if it is in both the “add” and “remove” set then timestamp in “remove” set should be less than that of the latest timestamp in “add” set. “Bias” comes into play, if timestamps are equal which can be towards “add” or “remove”. In this set, an element can be reinserted after being removed and thus, it has an advantage over 2P-Set. 93 | 94 | #### OR-Set (Observed-Removed Set) 95 | Similar to LWW-Element-Set, except that it unique tags are used instead of timestamps. For each element, a list of add/remove tags are maintained. An element is added by adding a newly generated unique tag to the add-tag list for the element. Removing an element involves copying all the tags in it’s add-tag list to it's remove-tag list. An element is a member of the set iff there exists a tag in add-tag list which is not in remove-tag list. 96 | 97 | #### Sequence CRDTs 98 | It involves an ordered set, list or a sequence of elements. This CRDT can be build on top of other Set based CRDTs by sorting them on some basis. 99 | We have used this CRDT to build a Collaborative Code/Text Editor. 100 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # http://www.sphinx-doc.org/en/master/config 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | 16 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 17 | 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = 'python3-crdt' 22 | copyright = '2019, Geetesh Gupta' 23 | author = 'Geetesh Gupta' 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = '1.0.3' 27 | 28 | master_doc = 'index' 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | 'sphinx.ext.autodoc', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # List of patterns, relative to source directory, that match files and 43 | # directories to ignore when looking for source files. 44 | # This pattern also affects html_static_path and html_extra_path. 45 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 46 | 47 | 48 | # -- Options for HTML output ------------------------------------------------- 49 | 50 | # The theme to use for HTML and HTML Help pages. See the documentation for 51 | # a list of builtin themes. 52 | # 53 | html_theme = 'sphinx_rtd_theme' 54 | 55 | # Add any paths that contain custom static files (such as style sheets) here, 56 | # relative to this directory. They are copied after the builtin static files, 57 | # so a file named "default.css" will overwrite the builtin "default.css". 58 | html_static_path = ['_static'] 59 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. python3-crdt documentation master file, created by 2 | sphinx-quickstart on Wed May 15 20:59:55 2019. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Python3-CRDT's documentation! 7 | ======================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | 14 | 15 | Indices and tables 16 | ================== 17 | 18 | * :ref:`genindex` 19 | * :ref:`modindex` 20 | * :ref:`search` 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | gg-python3-crdt 2 | =============== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | py3crdt 8 | tests 9 | -------------------------------------------------------------------------------- /docs/py3crdt.rst: -------------------------------------------------------------------------------- 1 | py3crdt package 2 | =============== 3 | 4 | Submodules 5 | ---------- 6 | 7 | py3crdt.gcounter module 8 | ----------------------- 9 | 10 | .. automodule:: py3crdt.gcounter 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | py3crdt.gset module 16 | ------------------- 17 | 18 | .. automodule:: py3crdt.gset 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | py3crdt.lww module 24 | ------------------ 25 | 26 | .. automodule:: py3crdt.lww 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | py3crdt.node module 32 | ------------------- 33 | 34 | .. automodule:: py3crdt.node 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | py3crdt.orset module 40 | -------------------- 41 | 42 | .. automodule:: py3crdt.orset 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | py3crdt.pncounter module 48 | ------------------------ 49 | 50 | .. automodule:: py3crdt.pncounter 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | py3crdt.sequence module 56 | ----------------------- 57 | 58 | .. automodule:: py3crdt.sequence 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | py3crdt.twopset module 64 | ---------------------- 65 | 66 | .. automodule:: py3crdt.twopset 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | 72 | Module contents 73 | --------------- 74 | 75 | .. automodule:: py3crdt 76 | :members: 77 | :undoc-members: 78 | :show-inheritance: 79 | -------------------------------------------------------------------------------- /docs/tests.rst: -------------------------------------------------------------------------------- 1 | tests package 2 | ============= 3 | 4 | Submodules 5 | ---------- 6 | 7 | tests.test\_gcounter module 8 | --------------------------- 9 | 10 | .. automodule:: tests.test_gcounter 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | tests.test\_gset module 16 | ----------------------- 17 | 18 | .. automodule:: tests.test_gset 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | tests.test\_lww module 24 | ---------------------- 25 | 26 | .. automodule:: tests.test_lww 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | tests.test\_orset module 32 | ------------------------ 33 | 34 | .. automodule:: tests.test_orset 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | tests.test\_pncounter module 40 | ---------------------------- 41 | 42 | .. automodule:: tests.test_pncounter 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | tests.test\_sequence module 48 | --------------------------- 49 | 50 | .. automodule:: tests.test_sequence 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | tests.test\_twopset module 56 | -------------------------- 57 | 58 | .. automodule:: tests.test_twopset 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | 64 | Module contents 65 | --------------- 66 | 67 | .. automodule:: tests 68 | :members: 69 | :undoc-members: 70 | :show-inheritance: 71 | -------------------------------------------------------------------------------- /py3crdt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshulahuja98/python3-crdt/b2e5d67580c562ae08d56127f41f7562e3ebe8e9/py3crdt/__init__.py -------------------------------------------------------------------------------- /py3crdt/gcounter.py: -------------------------------------------------------------------------------- 1 | class GCounter: 2 | """ 3 | Grow Only Counter CRDT Implementation. 4 | 5 | Notes: 6 | It implements an array of nodes where the value of array works as a counter. 7 | The value of array is sum of the values of the nodes in the array. 8 | Each node is assigned an ID equivalent to the index of the node in the array. 9 | The array is an equivalent for a cluster of nodes. 10 | Updating involves each node incrementing its own index value in the array. 11 | Merging occurs by taking the maximum of every node value in the cluster. 12 | Comparison function is included to verify the increments. 13 | Internal state is monotonically increased by application of each update function according to compare function. 14 | 15 | Attributes: 16 | payload (dict): Dict of node_key : node_value. 17 | id (any_type): ID of the class object. 18 | """ 19 | 20 | def __init__(self, id): 21 | self.payload = {} 22 | self.id = id 23 | 24 | def add_new_node(self, key): 25 | """ 26 | The function to add the key to the payload. 27 | 28 | Args: 29 | key (any_type): The key of the node to be added. 30 | 31 | Note: 32 | Initialize the key's value to 0 33 | """ 34 | 35 | self.payload[key] = 0 36 | 37 | def inc(self, key): 38 | """ 39 | The function to increment the key's value in payload. 40 | 41 | Args: 42 | key (any_type): The key of the node to be added. 43 | """ 44 | 45 | try: 46 | self.payload[key] += 1 47 | except Exception as e: 48 | print("{}".format(e)) 49 | 50 | def query(self): 51 | """ 52 | The function to return sum of the payload values. 53 | 54 | Returns: 55 | int: Sum of the payload values. 56 | """ 57 | 58 | return sum(self.payload.values()) 59 | 60 | def compare(self, gc2): 61 | """ 62 | The function to compare the payload value with argument's object's payload value. 63 | 64 | Args: 65 | gc2 (GCounter): The GCounter object to be compared. 66 | 67 | Returns: 68 | bool: True if sum of payload values is greater than that of argument's object, False otherwise. 69 | """ 70 | 71 | for key in self.payload: 72 | if self.payload[key] > gc2.payload[key]: 73 | return False 74 | 75 | def merge(self, gc2): 76 | """ 77 | The function to merge the GCounter object's payload with the argument's payload. 78 | 79 | Args: 80 | gc2 (GCounter): The GCounter object to be compared. 81 | 82 | Note: 83 | Merging occurs on the basis of the max value from the payloads for each key. 84 | """ 85 | 86 | new_payload = {key: 0 for key in self.payload} 87 | for key in self.payload: 88 | new_payload[key] = max(self.payload[key], gc2.payload[key]) 89 | self.payload = new_payload 90 | 91 | def display(self): 92 | """ 93 | The function to print the object's payload. 94 | """ 95 | 96 | print(self.payload.values()) 97 | -------------------------------------------------------------------------------- /py3crdt/gset.py: -------------------------------------------------------------------------------- 1 | class GSet: 2 | """ 3 | Grow Only Set CRDT Implementation. 4 | 5 | Notes: 6 | A set of elements where elements can only be added and once an element is added, it cannot be removed. 7 | Merging returns union of the two G-Sets. 8 | 9 | Attributes: 10 | payload (list): List of elements. 11 | id (any_type): ID of the class object. 12 | """ 13 | 14 | def __init__(self, id): 15 | self.payload = [] 16 | self.id = id 17 | 18 | def add(self, elem): 19 | """ 20 | The function to add the element to the payload. 21 | 22 | Args: 23 | elem (any_type): The element to be added. 24 | """ 25 | 26 | # Append the element to the payload. 27 | self.payload.append(elem) 28 | 29 | # Sort the payload. 30 | self.payload.sort() 31 | 32 | def query(self, elem): 33 | """ 34 | The function to return True if element is present in the payload. 35 | 36 | Args: 37 | elem (any_type): The element to be searched for. 38 | 39 | Returns: 40 | bool: True if element is present in the payload, False otherwise. 41 | """ 42 | 43 | return elem in self.payload 44 | 45 | def compare(self, gs2): 46 | """ 47 | The function to compare two GSet objects. 48 | 49 | Args: 50 | gs2 (GSet): The GSet object to be compared. 51 | 52 | Returns: 53 | bool: True if payloads of both objects are same, False otherwise. 54 | """ 55 | 56 | for elem in self.payload: 57 | if elem not in gs2.payload: 58 | return False 59 | return True 60 | 61 | def merge(self, gs2): 62 | """ 63 | The function to merge the GSet object's payload with the argument's payload. 64 | 65 | Args: 66 | gs2 (GSet): The GSet object to be compared. 67 | """ 68 | 69 | # Append the elements of argument's payload to the object's payload. 70 | for elem in gs2.payload: 71 | if elem not in self.payload: 72 | self.payload.append(elem) 73 | 74 | # Sort the payload. 75 | self.payload.sort() 76 | 77 | def display(self): 78 | """ 79 | The function to print the object's payload. 80 | """ 81 | 82 | print(self.payload) 83 | -------------------------------------------------------------------------------- /py3crdt/lww.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | 4 | class LWWFunctions: 5 | """ 6 | A class to provide static methods to LWWElementSet Class 7 | """ 8 | 9 | @staticmethod 10 | def update(payload, elem): 11 | """ 12 | The function to add an element to Payload. 13 | 14 | Args: 15 | payload (list): payload in which element has to be added. 16 | elem (any_type): The element to be added. 17 | 18 | Returns: 19 | payload (list): payload in which element is added. 20 | """ 21 | 22 | # Boolean to keep track if elem present in the payload 23 | elem_present = False 24 | 25 | for i in range(len(payload)): 26 | # If elem is present update the timestamp 27 | if payload[i]['elem'] == elem: 28 | payload[i]['timestamp'] = datetime.now() 29 | elem_present = True 30 | 31 | # If elem is not present add the elem 32 | if not elem_present: 33 | payload.append({'elem': elem, 'timestamp': datetime.now()}) 34 | 35 | payload.sort(key=lambda i: i['elem']) 36 | 37 | return payload 38 | 39 | @staticmethod 40 | def compare(payload1, payload2): 41 | """ 42 | The function to compare two LWW objects' payload. 43 | 44 | Args: 45 | payload1 (list): payload to be compared with. 46 | payload2 (list): payload to be compared to. 47 | 48 | Returns: 49 | bool: True if payload of both objects are same, False otherwise. 50 | """ 51 | 52 | for item_1 in payload1: 53 | if item_1 not in payload2: 54 | return False 55 | return True 56 | 57 | @staticmethod 58 | def merge(payload1, payload2): 59 | """ 60 | The function to merge the payload2 to payload1. 61 | 62 | Args: 63 | payload1 (list): payload to be merged to. 64 | payload2 (list): payload to be merged from. 65 | 66 | Returns: 67 | payload1 (list): payload merged to. 68 | """ 69 | 70 | # Append the elements of argument's payload to the object's payload. 71 | for item2 in payload2: 72 | 73 | # Boolean to keep track if item2 present in the payload1 74 | elem_found = False 75 | 76 | for i, item1 in enumerate(payload1): 77 | # If item2's elem is present and its timestamp is greater than that of item1, 78 | # update the timestamp 79 | if item1['elem'] == item2['elem']: 80 | elem_found = True 81 | 82 | if item1['timestamp'] < item2['timestamp']: 83 | payload1[i]['timestamp'] = item2['timestamp'] 84 | 85 | # If item2 is not present, add it to the payload1 86 | if not elem_found: 87 | payload1.append(item2) 88 | 89 | payload1.sort(key=lambda i: i['elem']) 90 | return payload1 91 | 92 | @staticmethod 93 | def display(name, payload): 94 | """ 95 | The function to print the object. 96 | 97 | Args: 98 | name (string): payload type. 99 | payload (list): payload to display. 100 | """ 101 | 102 | # Prints the type name of the payload 103 | print("{}: ".format(name), end="") 104 | 105 | # Prints elements with timestamps in microseconds 106 | for item in payload: 107 | print("{}:{}".format(item["elem"], item["timestamp"].microsecond), end=", ") 108 | 109 | # Prints a new line 110 | print() 111 | 112 | 113 | class LWWElementSet(): 114 | """ 115 | Last-Writer-Wins Element Set CRDT Implementation. 116 | 117 | Notes: 118 | Similar to 2P-Set except each element is added/removed with a timestamp. 119 | An element is a member of the set if it is in the “add” set but not in the “remove” set, 120 | or if it is in both the “add” and “remove” set 121 | then timestamp in “remove” set should be less than that of the latest timestamp in “add” set. 122 | “Bias” comes into play, if timestamps are equal which can be towards “add” or “remove”. 123 | In this set, an element can be reinserted after being removed and thus, it has an advantage over 2P-Set. 124 | 125 | Attributes: 126 | A (list): List of elements added. 127 | R (list): List of elements removed. 128 | id (any_type): ID of the class object. 129 | lwf (LWWFunctions): LWWFunctions object to access the static methods. 130 | """ 131 | 132 | def __init__(self, id): 133 | self.A = [] 134 | self.R = [] 135 | self.id = id 136 | self.lwwf = LWWFunctions() 137 | 138 | def add(self, elem): 139 | """ 140 | The function to add the element. 141 | 142 | Args: 143 | elem (any_type): The element to be added. 144 | 145 | Note: 146 | 'elem' is added to payload 'A' 147 | """ 148 | 149 | self.A = self.lwwf.update(self.A, elem) 150 | 151 | def remove(self, elem): 152 | """ 153 | The function to remove the element. 154 | 155 | Args: 156 | elem (any_type): The element to be removed. 157 | 158 | Note: 159 | 'elem' is added to payload 'R' 160 | """ 161 | 162 | self.R = self.lwwf.update(self.R, elem) 163 | 164 | def query(self, elem): 165 | """ 166 | The function to return True if element is present in the payload. 167 | 168 | Args: 169 | elem (any_type): The element to be searched for. 170 | 171 | Returns: 172 | bool: True if element present in the payload 'A' with latest timestamp than in payload 'R', False otherwise. 173 | """ 174 | 175 | elem_in_a = [item for item in self.A if item['elem'] == elem] 176 | if len(elem_in_a) != 0: 177 | elem_in_r = [item for item in self.R if item['elem'] == elem] 178 | if len(elem_in_r) == 0 or elem_in_r[-1]["timestamp"] < elem_in_a[-1]["timestamp"]: 179 | return True 180 | return False 181 | 182 | def compare(self, lww): 183 | """ 184 | The function to compare the payload with the argument's payload. 185 | 186 | Args: 187 | lww (LWWElementSet): Object to be compared to. 188 | 189 | Note: 190 | Compares payload 'A' and payload 'R' of the objects 191 | 192 | Returns: 193 | bool: True if payload of both objects are same, False otherwise. 194 | """ 195 | 196 | return self.lwwf.compare(self.A, lww.A) and self.lwwf.compare(self.R, lww.R) 197 | 198 | def merge(self, lww): 199 | """ 200 | The function to merge the payload with the argument's payload. 201 | 202 | Args: 203 | lww (LWWElementSet): Object to be merged from. 204 | """ 205 | 206 | # Merge payload 'A' 207 | self.A = self.lwwf.merge(self.A, lww.A) 208 | 209 | # Merge payload 'R' 210 | self.R = self.lwwf.merge(self.R, lww.R) 211 | 212 | def display(self): 213 | """ 214 | The function to print the object's payload. 215 | """ 216 | 217 | # Display payload 'A' 218 | self.lwwf.display('A', self.A) 219 | 220 | # Display payload 'R' 221 | self.lwwf.display('R', self.R) 222 | -------------------------------------------------------------------------------- /py3crdt/node.py: -------------------------------------------------------------------------------- 1 | class Node: 2 | """ 3 | A class to create a node with a id. 4 | 5 | Attributes: 6 | id (any_type): ID of the Node object. 7 | """ 8 | 9 | def __init__(self, id): 10 | self.id = id 11 | -------------------------------------------------------------------------------- /py3crdt/orset.py: -------------------------------------------------------------------------------- 1 | class ORSetFunctions: 2 | """ 3 | A class to provide static methods to ORSet Class 4 | """ 5 | 6 | @staticmethod 7 | def add(payload, elem, unique_tag): 8 | """ 9 | The function to add an element with it's unique tag to ORSet object's payload. 10 | 11 | Args: 12 | payload (list): Payload in which element has to be added. 13 | elem (any_type): The element to be added. 14 | unique_tag (any_type): Tag to identify element. 15 | 16 | Returns: 17 | payload (list): Payload in which element is added. 18 | """ 19 | 20 | # Bool value to check if element already in payload 21 | found = False 22 | for item in payload: 23 | 24 | # If element already in payload, add unique_tag to it's tag_list 25 | if elem == item["elem"]: 26 | item["tags"].append(unique_tag) 27 | found = True 28 | break 29 | 30 | # If element not in payload, add element with unique tag 31 | if not found: 32 | payload.append({"elem": elem, "tags": [unique_tag]}) 33 | 34 | # Sort the payload 35 | payload.sort(key=lambda i: i['elem']) 36 | 37 | return payload 38 | 39 | @staticmethod 40 | def remove(payloadA, payloadR, elem): 41 | """ 42 | The function to remove an element from ORSet object's payload. 43 | 44 | Args: 45 | payloadA (list): Payload in which elements to be added are added. 46 | payloadR (list): Payload in which elements to be removed are added. 47 | elem (any_type): The element to be removed. 48 | 49 | Note: 50 | It searches for element in payloadA, 51 | and copies it's tags to payloadR 52 | 53 | Returns: 54 | payloadR (list): Payload in which elements to be removed are added. 55 | """ 56 | 57 | # Search for element in payloadA and collect it's tags 58 | elem_tags = [] 59 | if len(payloadA): 60 | for item in payloadA: 61 | if elem == item["elem"]: 62 | elem_tags += item["tags"] 63 | break 64 | else: 65 | return 66 | 67 | # Bool value to check if element already in payloadR 68 | found = False 69 | for item in payloadR: 70 | 71 | # If element already in payloadR, merge tags from elem_tags list. 72 | if elem == item["elem"]: 73 | item["tags"] = item["tags"] + list(set(elem_tags) - set(item["tags"])) 74 | found = True 75 | break 76 | 77 | # If element not in payloadR, add element with elem_tags list 78 | if not found: 79 | payloadR.append({"elem": elem, "tags": elem_tags}) 80 | 81 | # Sort the payload 82 | payloadR.sort(key=lambda i: i['elem']) 83 | 84 | return payloadR 85 | 86 | @staticmethod 87 | def compare(payload1, payload2): 88 | """ 89 | The function to compare two ORSet objects' payloads. 90 | 91 | Args: 92 | payload1 (list): Payload to be compared with. 93 | payload2 (list): Payload to be compared to. 94 | 95 | Returns: 96 | bool: True if payloads of both objects are same, False otherwise. 97 | """ 98 | 99 | if len(payload1): 100 | for item1 in payload1: 101 | for item2 in payload2: 102 | if item1 != item2: 103 | return False 104 | else: 105 | # print("No elements added") 106 | return False 107 | return True 108 | 109 | @staticmethod 110 | def merge(payload1, payload2): 111 | """ 112 | The function to merge the payload2 to payload1. 113 | 114 | Args: 115 | payload1 (list): Payload to be merged to. 116 | payload2 (list): Payload to be merged from. 117 | 118 | Returns: 119 | payload1 (list): Payload merged to. 120 | """ 121 | 122 | for item2 in payload2: 123 | found = False 124 | for item1 in payload1: 125 | if item1['elem'] == item2['elem']: 126 | item1["tags"] = item1["tags"] + list(set(item2["tags"]) - set(item1["tags"])) 127 | found = True 128 | break 129 | if not found: 130 | payload1.append({"elem": item2["elem"], "tags": item2["tags"]}) 131 | 132 | # Sort the payload. 133 | payload1.sort(key=lambda i: i['elem']) 134 | 135 | return payload1 136 | 137 | @staticmethod 138 | def display(name, payload): 139 | """ 140 | The function to print the object. 141 | 142 | Args: 143 | name (string): Payload type. 144 | payload (list): Payload to display. 145 | 146 | Returns: 147 | -1: If no element in the payload 148 | """ 149 | 150 | # Prints the type name of the payload 151 | print("{}: ".format(name)) 152 | 153 | # Prints elements with timestamps in microseconds 154 | if len(payload): 155 | for item in payload: 156 | print("{}:{}".format(item["elem"], item["tags"])) 157 | pass 158 | else: 159 | # print("No elements to show") 160 | return -1 161 | 162 | @staticmethod 163 | def query(elem, payload): 164 | """ 165 | The function to return tags list if element is present in the payload. 166 | 167 | Args: 168 | elem (any_type): The element to be searched for. 169 | payload (list): Payload to query from. 170 | 171 | Returns: 172 | list: Tags list if element is present in the payload, Empty list otherwise. 173 | """ 174 | 175 | if len(payload): 176 | for item in payload: 177 | if elem == item["elem"]: 178 | return item["tags"] 179 | return [] 180 | else: 181 | # print("No elements to query") 182 | return [] 183 | 184 | 185 | class ORSet(): 186 | """ 187 | Observed-Removed Set CRDT Implementation. 188 | 189 | Notes: 190 | Similar to LWW-Element-Set, except that it unique tags are used instead of timestamps. 191 | For each element, a list of add/remove tags are maintained. 192 | An element is added by adding a newly generated unique tag to the add-tag list for the element. 193 | Removing an element involves copying all the tags in it’s add-tag list to it's remove-tag list. 194 | An element is a member of the set iff there exists a tag in add-tag list which is not in remove-tag list. 195 | 196 | Attributes: 197 | A (list): List of elements added. 198 | R (list): List of elements removed. 199 | id (any_type): ID of the class object. 200 | orsetf (ORSetFunctions): ORSetFunctions object to access the static methods. 201 | """ 202 | 203 | def __init__(self, id): 204 | self.A = [] 205 | self.R = [] 206 | self.id = id 207 | self.orsetf = ORSetFunctions() 208 | 209 | def add(self, elem, unique_tag): 210 | """ 211 | The function to add the element. 212 | 213 | Args: 214 | elem (any_type): The element to be added. 215 | unique_tag (any_type): Tag to identify element. 216 | 217 | Note: 218 | 'elem' is added to payload 'A' 219 | """ 220 | 221 | self.A = self.orsetf.add(self.A, elem, unique_tag) 222 | 223 | def remove(self, elem): 224 | """ 225 | The function to remove the element. 226 | 227 | Args: 228 | elem (any_type): The element to be removed. 229 | 230 | Note: 231 | 'elem' is added to payload 'R' 232 | """ 233 | 234 | self.R = self.orsetf.remove(self.A, self.R, elem) 235 | 236 | def query(self, elem): 237 | """ 238 | The function to return True if element is present in the payload. 239 | 240 | Args: 241 | elem (any_type): The element to be searched for. 242 | 243 | Returns: 244 | bool: True if element's tags present in the payload 'A' but not in payload 'R', False otherwise. 245 | """ 246 | 247 | if set(self.orsetf.query(elem, self.A)) - set(self.orsetf.query(elem, self.R)): 248 | return True 249 | return False 250 | 251 | def compare(self, orset): 252 | """ 253 | The function to compare the payloads with the argument's payloads. 254 | 255 | Args: 256 | orset (ORSet): Object to be compared to. 257 | 258 | Note: 259 | Compares payload 'A' and payload 'R' of the objects 260 | 261 | Returns: 262 | bool: True if payloads of both objects are same, False otherwise. 263 | """ 264 | 265 | return self.orsetf.compare(self.A, orset.A) and self.orsetf.compare(self.A, orset.A) 266 | 267 | def merge(self, orset): 268 | """ 269 | The function to merge the payloads with the argument's payloads. 270 | 271 | Args: 272 | orset (ORSet): Object to be merged from. 273 | """ 274 | 275 | # Merge payload 'A' 276 | self.A = self.orsetf.merge(self.A, orset.A) 277 | 278 | # Merge payload 'R' 279 | self.R = self.orsetf.merge(self.R, orset.R) 280 | 281 | def display(self): 282 | """ 283 | The function to print the object's payloads. 284 | """ 285 | 286 | # Display payload 'A' 287 | self.orsetf.display('A', self.A) 288 | 289 | # Display payload 'R' 290 | self.orsetf.display('R', self.R) 291 | -------------------------------------------------------------------------------- /py3crdt/pncounter.py: -------------------------------------------------------------------------------- 1 | # Import PNCounter 2 | from .gcounter import GCounter 3 | 4 | 5 | class PNCounter: 6 | """ 7 | Positive-Negative Counter CRDT Implementation. 8 | 9 | Notes: 10 | This counter supports both increment and decrement operations. 11 | It combines two G-Counters namely “P” (for incrementing) and “N” (for decrementing) counter. 12 | The value of the counter is the value of the P counter minus the value of the N counter. 13 | Merging involves merging the P and N counter independently. 14 | 15 | Attributes: 16 | P (PNCounter): PNCounter object to increment counter. 17 | N (PNCounter): PNCounter object to deceremnt counter. 18 | id (any_type): ID of the class object. 19 | """ 20 | 21 | def __init__(self, id): 22 | self.P = GCounter(id) 23 | self.N = GCounter(id) 24 | self.id = id 25 | 26 | def add_new_node(self, key): 27 | """ 28 | The function to add the key to the payload. 29 | 30 | Args: 31 | key (any_type): The key of the node to be added. 32 | 33 | Note: 34 | Adds the key to both gcounter objects P and N. 35 | """ 36 | 37 | self.P.add_new_node(key) 38 | self.N.add_new_node(key) 39 | 40 | def inc(self, key): 41 | """ 42 | The function to increment the key's value in payload. 43 | 44 | Args: 45 | key (any_type): The key of the node to be added. 46 | 47 | Note: 48 | Increments the value of gcounter object P. 49 | """ 50 | 51 | self.P.inc(key) 52 | 53 | def dec(self, key): 54 | """ 55 | The function to decrement the key's value in payload. 56 | 57 | Args: 58 | key (any_type): The key of the node to be added. 59 | 60 | Note: 61 | Increments the value of gcounter object N. 62 | """ 63 | 64 | self.N.inc(key) 65 | 66 | def query(self): 67 | """ 68 | The function to return the effective counter value. 69 | 70 | Note: 71 | Returns the difference between gcounter object P and N. 72 | """ 73 | 74 | return self.P.query() - self.N.query() 75 | 76 | def compare(self, pnc2): 77 | """ 78 | The function to compare the payload value with argument's object's payload value. 79 | 80 | Args: 81 | pnc2 (PNCounter): The PNCounter object to be compared. 82 | 83 | Returns: 84 | bool: True if effective payload value is greater than that of argument's object, False otherwise. 85 | """ 86 | 87 | return self.P.compare(pnc2.P) and self.N.compare(pnc2.N) 88 | 89 | def merge(self, pnc2): 90 | """ 91 | The function to merge the PNCounter object's payload with the argument's payload. 92 | 93 | Args: 94 | pnc2 (PNCounter): The PNCounter object to be compared. 95 | 96 | Note: 97 | Merging occurs on the basis of the max value from the payloads for each key. 98 | Merges both objects P and N. 99 | """ 100 | 101 | self.P.merge(pnc2.P) 102 | self.N.merge(pnc2.N) 103 | 104 | def display(self, name): 105 | """ 106 | The function to print the object's payloads. 107 | """ 108 | 109 | # Display object P 110 | print("{}.P: ".format(name), end="") 111 | self.P.display() 112 | 113 | # Display object N 114 | print("{}.N: ".format(name), end="") 115 | self.N.display() 116 | -------------------------------------------------------------------------------- /py3crdt/sequence.py: -------------------------------------------------------------------------------- 1 | class SeqFunctions: 2 | """ 3 | A class to provide static methods to Sequence Class 4 | """ 5 | 6 | @staticmethod 7 | def add(payload, elem, id): 8 | """ 9 | The function to add an element with it's ID to Sequence object's payload. 10 | 11 | Args: 12 | payload (list): Payload in which element has to be added. 13 | elem (any_type): The element to be added. 14 | id (any_type): ID of the element. 15 | 16 | Returns: 17 | payload (list): Payload in which element is added. 18 | """ 19 | 20 | # Add the element to the payload 21 | payload.append((elem, id)) 22 | 23 | # Sort the payload 24 | payload.sort(key=lambda i: i[1]) 25 | 26 | return payload 27 | 28 | @staticmethod 29 | def remove(payload, id): 30 | """ 31 | The function to remove an element from Sequence object's payload. 32 | 33 | Args: 34 | payload (list): Payload in which elements to be removed are added. 35 | id (any_type): ID of the element to be removed. 36 | 37 | Returns: 38 | payload (list): Payload in which elements to be removed are added. 39 | """ 40 | 41 | # Add the ID to the payload 42 | payload.append(id) 43 | 44 | # Sort the payload 45 | payload.sort() 46 | 47 | return payload 48 | 49 | @staticmethod 50 | def merge(payload1, payload2): 51 | """ 52 | The function to merge the payload2 to payload1. 53 | 54 | Args: 55 | payload1 (list): Payload to be merged to. 56 | payload2 (list): Payload to be merged from. 57 | 58 | Returns: 59 | payload1 (list): Payload merged to. 60 | """ 61 | 62 | for item in payload2: 63 | if item not in payload1: 64 | payload1.append(item) 65 | 66 | return payload1 67 | 68 | @staticmethod 69 | def display(name, payload): 70 | """ 71 | The function to print the object. 72 | 73 | Args: 74 | name (string): Payload type. 75 | payload (list): Payload to display. 76 | """ 77 | 78 | print("{}: ".format(name), payload) 79 | 80 | @staticmethod 81 | def get_seq(payload): 82 | """ 83 | The function to return a string of elements in the payload. 84 | 85 | Args: 86 | payload (list): Payload to display. 87 | 88 | Returns: 89 | seq (string): String of elements in the payload 90 | """ 91 | 92 | seq = "" 93 | for elem in payload: 94 | seq += elem 95 | return seq 96 | 97 | 98 | class Sequence(): 99 | """ 100 | Sequence CRDT Implementation. 101 | 102 | Notes: 103 | An ordered set, list or a sequence of elements. 104 | This CRDT can be build on top of other Set based CRDTs by sorting them on some basis. 105 | We have used this CRDT to build a Collaborative Code/Text Editor. 106 | 107 | Attributes: 108 | elem_list (list): List of elements added. 109 | id_remv_list (list): List of IDs removed. 110 | id_seq (list): List of IDs in sequence. 111 | id_elem_seq (list): List of elements in sequence. 112 | id (any_type): ID of the class object. 113 | seqf (SeqFunctions): SeqFunctions object to access the static methods. 114 | """ 115 | 116 | def __init__(self, id): 117 | self.elem_list = [] 118 | self.id_remv_list = [] 119 | self.id_seq = [] 120 | self.elem_seq = [] 121 | self.id = id 122 | self.seqf = SeqFunctions() 123 | 124 | def update_seq(self): 125 | for item in self.elem_list: 126 | if item[1] not in self.id_remv_list and item[1] not in self.id_seq: 127 | self.id_seq.append(item[1]) 128 | for id in self.id_remv_list: 129 | if id in self.id_seq: 130 | del self.elem_seq[self.id_seq.index(id)] 131 | self.id_seq.remove(id) 132 | self.id_seq.sort() 133 | for id in self.id_seq: 134 | for item in self.elem_list: 135 | if item[1] == id: 136 | if len(self.elem_seq) > self.id_seq.index(id): 137 | if item[0] != self.elem_seq[self.id_seq.index(id)]: 138 | self.elem_seq.insert(self.id_seq.index(id), item[0]) 139 | else: 140 | self.elem_seq.append(item[0]) 141 | 142 | def add(self, elem, id): 143 | """ 144 | The function to add the element. 145 | 146 | Args: 147 | elem (any_type): The element to be added. 148 | id (any_type): ID of the element. 149 | 150 | Note: 151 | 'elem' is added to elem_list 152 | """ 153 | 154 | self.elem_list = self.seqf.add(self.elem_list, elem, id) 155 | 156 | # Call update_seq function 157 | self.update_seq() 158 | 159 | def remove(self, id): 160 | """ 161 | The function to remove the element. 162 | 163 | Args: 164 | id (any_type): The ID of the element to be removed. 165 | 166 | Note: 167 | 'elem' is added to id_remv_list 168 | """ 169 | 170 | self.id_remv_list = self.seqf.remove(self.id_remv_list, id) 171 | 172 | # Call update_seq function 173 | self.update_seq() 174 | 175 | def query(self, id): 176 | """ 177 | The function to return True if ID of the element is present in the list. 178 | 179 | Args: 180 | elem (any_type): The element to be searched for. 181 | 182 | Returns: 183 | bool: True if element's ID present in the elem_list but not in id_remv_list , False otherwise. 184 | """ 185 | 186 | for item in self.elem_list: 187 | if item[1] == id: 188 | if id not in self.id_remv_list: 189 | return True 190 | else: 191 | return False 192 | return False 193 | 194 | def merge(self, list, func='na'): 195 | """ 196 | The function to merge the lists with the argument's list. 197 | 198 | Args: 199 | list ( 200 | list: List to be merged from, 201 | Sequence: Object to be merged from. 202 | ) 203 | func ( 204 | 'na': Merge both elem_list and id_remv_list, 205 | 'elem': Merge elem_list, 206 | 'id': Merge id_remv_list, 207 | ) 208 | """ 209 | 210 | if func == 'na': 211 | self.elem_list = self.seqf.merge(self.elem_list, list.elem_list) 212 | self.id_remv_list = self.seqf.merge(self.id_remv_list, list.id_remv_list) 213 | elif func == 'elem': 214 | self.elem_list = self.seqf.merge(self.elem_list, list) 215 | elif func == 'id': 216 | self.id_remv_list = self.seqf.merge(self.id_remv_list, list) 217 | self.update_seq() 218 | 219 | def display(self): 220 | """ 221 | The function to print the object's payloads. 222 | """ 223 | 224 | self.seqf.display("Elem List", self.elem_list) 225 | self.seqf.display("ID Removed List", self.id_remv_list) 226 | self.seqf.display("ID Seq", self.id_seq) 227 | self.seqf.display("Elem Seq", self.elem_seq) 228 | 229 | def get_seq(self): 230 | """ 231 | The function to get the sequence as string. 232 | """ 233 | 234 | return self.seqf.get_seq(self.elem_seq) 235 | -------------------------------------------------------------------------------- /py3crdt/twopset.py: -------------------------------------------------------------------------------- 1 | # Import GSet 2 | from .gset import GSet 3 | 4 | 5 | class TwoPSet: 6 | """ 7 | Two-Phase Set CRDT Implementation. 8 | 9 | Notes: 10 | A set in which elements can be added as well as removed. It combines two G-Sets namely “add” and “remove” set. 11 | For adding/removing an element, it is inserted in the “add”/“remove” set. 12 | An element is a member of the set if it is in the “add” set but not in the “remove” set. 13 | Query function returns whether the element is a member of the set or not. 14 | Hence, if an element is removed, query will never return True for that element, so it cannot be re-added. 15 | Merging involves union of the “add”/“remove” sets. 16 | 17 | Attributes: 18 | A (list): List of elements added. 19 | R (list): List of elements removed. 20 | id (any_type): ID of the class object. 21 | """ 22 | 23 | def __init__(self, id): 24 | self.A = GSet(id) 25 | self.R = GSet(id) 26 | self.id = id 27 | 28 | def add(self, elem): 29 | """ 30 | The function to add the element. 31 | 32 | Args: 33 | elem (any_type): The element to be added. 34 | 35 | Note: 36 | 'elem' is added to payload 'A' 37 | """ 38 | 39 | self.A.add(elem) 40 | 41 | def remove(self, elem): 42 | """ 43 | The function to remove the element. 44 | 45 | Args: 46 | elem (any_type): The element to be removed. 47 | 48 | Note: 49 | 'elem' is added to payload 'R' 50 | """ 51 | 52 | self.R.add(elem) 53 | 54 | def query(self, elem): 55 | """ 56 | The function to return True if element is present in the payload. 57 | 58 | Args: 59 | elem (any_type): The element to be searched for. 60 | 61 | Returns: 62 | bool: True if element's tags present in the payload 'A' but not in payload 'R', False otherwise. 63 | """ 64 | 65 | return self.A.query(elem) and not self.R.query(elem) 66 | 67 | def compare(self, tps2): 68 | """ 69 | The function to compare the payloads with the argument's payloads. 70 | 71 | Args: 72 | tps2 (TwoPSet): Object to be compared to. 73 | 74 | Note: 75 | Compares payload 'A' and payload 'R' of the objects 76 | 77 | Returns: 78 | bool: True if payloads of both objects are same, False otherwise. 79 | """ 80 | 81 | return self.A.compare(tps2.A) and self.R.compare(tps2.R) 82 | 83 | def merge(self, tps2): 84 | """ 85 | The function to merge the payloads with the argument's payloads. 86 | 87 | Args: 88 | tps2 (TwoPSet): Object to be merged from. 89 | """ 90 | 91 | # Merge payload 'A' 92 | self.A.merge(tps2.A) 93 | 94 | # Merge payload 'R' 95 | self.R.merge(tps2.R) 96 | 97 | def display(self): 98 | """ 99 | The function to print the object's payloads. 100 | """ 101 | 102 | # Display payload 'A' 103 | print("A: ", end="") 104 | self.A.display() 105 | 106 | # Display payload 'R' 107 | print("R: ", end="") 108 | self.R.display() 109 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="python3-crdt", 8 | version="1.0.3", 9 | author="Geetesh Gupta", 10 | author_email="ggguitarg31@gmail.com", 11 | description="A python library for CRDTs (Conflict-free Replicated Data types)", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/geetesh-gupta/python3-crdt", 15 | packages=setuptools.find_packages(), 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | "Intended Audience :: Science/Research", 21 | "Intended Audience :: Developers", 22 | "Topic :: Software Development :: Libraries :: Python Modules", 23 | ], 24 | 25 | ) 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anshulahuja98/python3-crdt/b2e5d67580c562ae08d56127f41f7562e3ebe8e9/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_gcounter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import uuid 3 | import py3crdt 4 | from py3crdt.gcounter import GCounter 5 | from py3crdt.node import Node 6 | 7 | 8 | class TestGCounter(unittest.TestCase): 9 | def setUp(self): 10 | self.node1 = Node(uuid.uuid4()) 11 | self.node2 = Node(uuid.uuid4()) 12 | 13 | # Create a GCounter 14 | self.gc1 = GCounter(uuid.uuid4()) 15 | 16 | # Add nodes to gc1 17 | self.gc1.add_new_node(self.node1.id) 18 | self.gc1.add_new_node(self.node2.id) 19 | 20 | # Create another GCounter 21 | self.gc2 = GCounter(uuid.uuid4()) 22 | # Add nodes to gc2 23 | self.gc2.add_new_node(self.node1.id) 24 | self.gc2.add_new_node(self.node2.id) 25 | 26 | # Increment gc1 values for each node 27 | self.gc1.inc(self.node1.id) 28 | self.gc1.inc(self.node1.id) 29 | self.gc1.inc(self.node2.id) 30 | # Increment gc2 values for each node 31 | self.gc2.inc(self.node1.id) 32 | self.gc2.inc(self.node2.id) 33 | self.gc2.inc(self.node2.id) 34 | self.gc2.inc(self.node2.id) 35 | 36 | def test_check_increment(self): 37 | self.assertEqual(self.gc1.payload[self.node1.id], 2) 38 | self.assertEqual(self.gc1.payload[self.node2.id], 1) 39 | self.assertEqual(self.gc2.payload[self.node1.id], 1) 40 | self.assertEqual(self.gc2.payload[self.node2.id], 3) 41 | 42 | def test_merging_gcounters(self): 43 | # Check gc2 merging 44 | self.gc2.merge(self.gc1) 45 | self.assertEqual(self.gc2.payload[self.node1.id], 2) 46 | self.assertEqual(self.gc2.payload[self.node2.id], 3) 47 | # Check gc1 merging 48 | self.gc1.merge(self.gc2) 49 | self.assertEqual(self.gc1.payload[self.node1.id], 2) 50 | self.assertEqual(self.gc1.payload[self.node2.id], 3) 51 | # Check if they are both equal 52 | self.assertEqual(self.gc1.payload, self.gc2.payload) 53 | 54 | if __name__ == '__main__': 55 | unittest.main() 56 | -------------------------------------------------------------------------------- /tests/test_gset.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import uuid 3 | import py3crdt 4 | from py3crdt.gset import GSet 5 | 6 | 7 | class TestLWW(unittest.TestCase): 8 | def setUp(self): 9 | # Create a GSet 10 | self.gset1 = GSet(uuid.uuid4()) 11 | 12 | # Create another GSet 13 | self.gset2 = GSet(uuid.uuid4()) 14 | 15 | # Add elements to gset1 16 | self.gset1.add('a') 17 | self.gset1.add('b') 18 | 19 | # Add elements to gset1 20 | self.gset2.add('b') 21 | self.gset2.add('c') 22 | self.gset2.add('d') 23 | 24 | def test_elements_add_correctly_gset(self): 25 | self.assertEqual(self.gset1.payload, ['a', 'b']) 26 | self.assertEqual(self.gset2.payload, ['b', 'c', 'd']) 27 | 28 | def test_querying_gset_without_merging(self): 29 | # Check gset1 querying 30 | self.assertTrue(self.gset1.query('a')) 31 | self.assertTrue(self.gset1.query('b')) 32 | self.assertFalse(self.gset1.query('c')) 33 | self.assertFalse(self.gset1.query('d')) 34 | 35 | # Check gset2 querying 36 | self.assertFalse(self.gset2.query('a')) 37 | self.assertTrue(self.gset2.query('b')) 38 | self.assertTrue(self.gset2.query('c')) 39 | self.assertTrue(self.gset2.query('d')) 40 | 41 | def test_merging_gset(self): 42 | # Check gset1 merging 43 | self.gset1.merge(self.gset2) 44 | self.assertEqual(self.gset1.payload, ['a', 'b', 'c', 'd']) 45 | 46 | # Check gset2 merging 47 | self.gset2.merge(self.gset1) 48 | self.assertEqual(self.gset2.payload, ['a', 'b', 'c', 'd']) 49 | 50 | # Check if they are both equal 51 | self.assertEqual(self.gset1.payload, self.gset2.payload) 52 | 53 | def test_querying_gset_with_merging(self): 54 | # Check gset2 merging 55 | self.gset2.merge(self.gset1) 56 | self.assertTrue(self.gset2.query('a')) 57 | self.assertTrue(self.gset2.query('b')) 58 | self.assertTrue(self.gset2.query('c')) 59 | self.assertTrue(self.gset2.query('d')) 60 | 61 | # Check gset1 merging 62 | self.gset1.merge(self.gset2) 63 | self.assertTrue(self.gset1.query('a')) 64 | self.assertTrue(self.gset1.query('b')) 65 | self.assertTrue(self.gset1.query('c')) 66 | self.assertTrue(self.gset1.query('d')) 67 | 68 | 69 | if __name__ == '__main__': 70 | unittest.main() 71 | -------------------------------------------------------------------------------- /tests/test_lww.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import uuid 3 | import py3crdt 4 | from py3crdt.lww import LWWElementSet as LWWSet 5 | 6 | 7 | class TestLWW(unittest.TestCase): 8 | def setUp(self): 9 | # Create a LWWSet 10 | self.lww1 = LWWSet(uuid.uuid4()) 11 | 12 | # Create another LWWSet 13 | self.lww2 = LWWSet(uuid.uuid4()) 14 | 15 | # Add elements to lww1 16 | self.lww1.add('a') 17 | self.lww1.add('b') 18 | 19 | # Add elements to lww1 20 | self.lww2.add('b') 21 | self.lww2.add('c') 22 | self.lww2.add('d') 23 | 24 | def test_elements_add_correctly_lww_set(self): 25 | self.assertEqual([_['elem'] for _ in self.lww1.A], ['a', 'b']) 26 | self.assertEqual([_['elem'] for _ in self.lww1.R], []) 27 | self.assertEqual([_['elem'] for _ in self.lww2.A], ['b', 'c', 'd']) 28 | self.assertEqual([_['elem'] for _ in self.lww2.R], []) 29 | 30 | def test_querying_lww_set_without_removal_and_merging(self): 31 | # Check lww1 querying 32 | self.assertTrue(self.lww1.query('a')) 33 | self.assertTrue(self.lww1.query('b')) 34 | self.assertFalse(self.lww1.query('c')) 35 | self.assertFalse(self.lww1.query('d')) 36 | 37 | # Check lww2 querying 38 | self.assertFalse(self.lww2.query('a')) 39 | self.assertTrue(self.lww2.query('b')) 40 | self.assertTrue(self.lww2.query('c')) 41 | self.assertTrue(self.lww2.query('d')) 42 | 43 | def test_merging_lww_set_without_removal(self): 44 | # Check lww1 merging 45 | self.lww1.merge(self.lww2) 46 | self.assertEqual([_['elem'] for _ in self.lww1.A], ['a', 'b', 'c', 'd']) 47 | self.assertEqual([_['elem'] for _ in self.lww1.R], []) 48 | 49 | # Check lww2 merging 50 | self.lww2.merge(self.lww1) 51 | self.assertEqual([_['elem'] for _ in self.lww2.A], ['a', 'b', 'c', 'd']) 52 | self.assertEqual([_['elem'] for _ in self.lww2.R], []) 53 | 54 | # Check if they are both equal 55 | self.assertEqual([_['elem'] for _ in self.lww1.A], [_['elem'] for _ in self.lww2.A]) 56 | self.assertEqual([_['elem'] for _ in self.lww1.R], [_['elem'] for _ in self.lww2.R]) 57 | 58 | def test_querying_lww_set_with_merging_without_removal(self): 59 | # Check lww2 merging 60 | self.lww2.merge(self.lww1) 61 | self.assertTrue(self.lww2.query('a')) 62 | self.assertTrue(self.lww2.query('b')) 63 | self.assertTrue(self.lww2.query('c')) 64 | self.assertTrue(self.lww2.query('d')) 65 | 66 | # Check lww1 merging 67 | self.lww1.merge(self.lww2) 68 | self.assertTrue(self.lww1.query('a')) 69 | self.assertTrue(self.lww1.query('b')) 70 | self.assertTrue(self.lww1.query('c')) 71 | self.assertTrue(self.lww1.query('d')) 72 | 73 | def test_elements_remove_correctly_lww_set(self): 74 | # Remove elements from lww1 75 | self.lww1.remove('b') 76 | 77 | self.assertEqual([_['elem'] for _ in self.lww1.A], ['a', 'b']) 78 | self.assertEqual([_['elem'] for _ in self.lww1.R], ['b']) 79 | 80 | # Remove elements from lww2 81 | self.lww2.remove('b') 82 | self.lww2.remove('c') 83 | 84 | self.assertEqual([_['elem'] for _ in self.lww2.A], ['b', 'c', 'd']) 85 | self.assertEqual([_['elem'] for _ in self.lww2.R], ['b', 'c']) 86 | 87 | def test_querying_lww_set_without_merging_with_removal(self): 88 | # Remove elements from lww1 89 | self.lww1.remove('b') 90 | 91 | # Check lww1 querying 92 | self.assertTrue(self.lww1.query('a')) 93 | self.assertFalse(self.lww1.query('b')) 94 | self.assertFalse(self.lww1.query('c')) 95 | self.assertFalse(self.lww1.query('d')) 96 | 97 | # Remove elements from lww2 98 | self.lww2.remove('b') 99 | self.lww2.remove('c') 100 | 101 | # Check lww2 querying 102 | self.assertFalse(self.lww2.query('a')) 103 | self.assertFalse(self.lww2.query('b')) 104 | self.assertFalse(self.lww2.query('c')) 105 | self.assertTrue(self.lww2.query('d')) 106 | 107 | def test_merging_lww_set_with_removal(self): 108 | # Remove elements from lww1 109 | self.lww1.remove('b') 110 | 111 | # Remove elements from lww2 112 | self.lww2.remove('b') 113 | self.lww2.remove('c') 114 | 115 | # Check lww1 merging 116 | self.lww1.merge(self.lww2) 117 | self.assertEqual([_['elem'] for _ in self.lww1.A], ['a', 'b', 'c', 'd']) 118 | self.assertEqual([_['elem'] for _ in self.lww1.R], ['b', 'c']) 119 | 120 | # Check lww2 merging 121 | self.lww2.merge(self.lww1) 122 | self.assertEqual([_['elem'] for _ in self.lww2.A], ['a', 'b', 'c', 'd']) 123 | self.assertEqual([_['elem'] for _ in self.lww2.R], ['b', 'c']) 124 | 125 | # Check if they are both equal 126 | self.assertEqual([_['elem'] for _ in self.lww1.A], [_['elem'] for _ in self.lww2.A]) 127 | self.assertEqual([_['elem'] for _ in self.lww1.R], [_['elem'] for _ in self.lww2.R]) 128 | 129 | def test_querying_lww_set_with_merging_with_removal(self): 130 | # Remove elements from lww1 131 | self.lww1.remove('b') 132 | 133 | # Remove elements from lww2 134 | self.lww2.remove('b') 135 | self.lww2.remove('c') 136 | 137 | # Merge lww2 to lww1 138 | self.lww1.merge(self.lww2) 139 | 140 | # Merge lww1 to lww2 141 | self.lww2.merge(self.lww1) 142 | 143 | # Check lww1 querying 144 | self.assertTrue(self.lww1.query('a')) 145 | self.assertFalse(self.lww1.query('b')) 146 | self.assertFalse(self.lww1.query('c')) 147 | self.assertTrue(self.lww1.query('d')) 148 | 149 | # Check lww2 querying 150 | self.assertTrue(self.lww2.query('a')) 151 | self.assertFalse(self.lww2.query('b')) 152 | self.assertFalse(self.lww2.query('c')) 153 | self.assertTrue(self.lww2.query('d')) 154 | 155 | 156 | if __name__ == '__main__': 157 | unittest.main() 158 | -------------------------------------------------------------------------------- /tests/test_orset.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import uuid 3 | import py3crdt 4 | from py3crdt.orset import ORSet 5 | 6 | 7 | class TestORSet(unittest.TestCase): 8 | def setUp(self): 9 | # Create a ORSet 10 | self.orset1 = ORSet(uuid.uuid4()) 11 | 12 | # Create another ORSet 13 | self.orset2 = ORSet(uuid.uuid4()) 14 | 15 | # Add elements to orset1 16 | self.orset1.add('a', uuid.uuid4()) 17 | self.orset1.add('b', uuid.uuid4()) 18 | 19 | # Add elements to orset1 20 | self.orset2.add('b', uuid.uuid4()) 21 | self.orset2.add('c', uuid.uuid4()) 22 | self.orset2.add('d', uuid.uuid4()) 23 | 24 | def test_elements_add_correctly_orset(self): 25 | self.assertEqual([_['elem'] for _ in self.orset1.A], ['a', 'b']) 26 | self.assertEqual([_['elem'] for _ in self.orset1.R], []) 27 | self.assertEqual([_['elem'] for _ in self.orset2.A], ['b', 'c', 'd']) 28 | self.assertEqual([_['elem'] for _ in self.orset2.R], []) 29 | 30 | def test_querying_orset_without_removal_and_merging(self): 31 | # Check orset1 querying 32 | self.assertTrue(self.orset1.query('a')) 33 | self.assertTrue(self.orset1.query('b')) 34 | self.assertFalse(self.orset1.query('c')) 35 | self.assertFalse(self.orset1.query('d')) 36 | 37 | # Check orset2 querying 38 | self.assertFalse(self.orset2.query('a')) 39 | self.assertTrue(self.orset2.query('b')) 40 | self.assertTrue(self.orset2.query('c')) 41 | self.assertTrue(self.orset2.query('d')) 42 | 43 | def test_merging_orset_without_removal(self): 44 | # Check orset1 merging 45 | self.orset1.merge(self.orset2) 46 | self.assertEqual([_['elem'] for _ in self.orset1.A], ['a', 'b', 'c', 'd']) 47 | for _ in self.orset1.A: 48 | if _['elem'] == 'b': 49 | self.assertEqual(len(_['tags']), 2) 50 | break 51 | self.assertEqual([_['elem'] for _ in self.orset1.R], []) 52 | 53 | # Check orset2 merging 54 | self.orset2.merge(self.orset1) 55 | self.assertEqual([_['elem'] for _ in self.orset2.A], ['a', 'b', 'c', 'd']) 56 | for _ in self.orset2.A: 57 | if _['elem'] == 'b': 58 | self.assertEqual(len(_['tags']), 2) 59 | break 60 | self.assertEqual([_['elem'] for _ in self.orset2.R], []) 61 | 62 | # Check if they are both equal 63 | self.assertEqual([_['elem'] for _ in self.orset1.A], [_['elem'] for _ in self.orset2.A]) 64 | self.assertEqual([_['elem'] for _ in self.orset1.R], [_['elem'] for _ in self.orset2.R]) 65 | 66 | def test_querying_orset_with_merging_without_removal(self): 67 | # Check orset2 merging 68 | self.orset2.merge(self.orset1) 69 | self.assertTrue(self.orset2.query('a')) 70 | self.assertTrue(self.orset2.query('b')) 71 | self.assertTrue(self.orset2.query('c')) 72 | self.assertTrue(self.orset2.query('d')) 73 | 74 | # Check orset1 merging 75 | self.orset1.merge(self.orset2) 76 | self.assertTrue(self.orset1.query('a')) 77 | self.assertTrue(self.orset1.query('b')) 78 | self.assertTrue(self.orset1.query('c')) 79 | self.assertTrue(self.orset1.query('d')) 80 | 81 | def test_elements_remove_correctly_orset(self): 82 | # Remove elements from orset1 83 | self.orset1.remove('b') 84 | 85 | self.assertEqual([_['elem'] for _ in self.orset1.A], ['a', 'b']) 86 | self.assertEqual([_['elem'] for _ in self.orset1.R], ['b']) 87 | 88 | # Remove elements from orset2 89 | self.orset2.remove('b') 90 | self.orset2.remove('c') 91 | 92 | self.assertEqual([_['elem'] for _ in self.orset2.A], ['b', 'c', 'd']) 93 | self.assertEqual([_['elem'] for _ in self.orset2.R], ['b', 'c']) 94 | 95 | def test_querying_orset_without_merging_with_removal(self): 96 | # Remove elements from orset1 97 | self.orset1.remove('b') 98 | 99 | # Check orset1 querying 100 | self.assertTrue(self.orset1.query('a')) 101 | self.assertFalse(self.orset1.query('b')) 102 | self.assertFalse(self.orset1.query('c')) 103 | self.assertFalse(self.orset1.query('d')) 104 | 105 | # Remove elements from orset2 106 | self.orset2.remove('b') 107 | self.orset2.remove('c') 108 | 109 | # Check orset2 querying 110 | self.assertFalse(self.orset2.query('a')) 111 | self.assertFalse(self.orset2.query('b')) 112 | self.assertFalse(self.orset2.query('c')) 113 | self.assertTrue(self.orset2.query('d')) 114 | 115 | def test_merging_orset_with_removal(self): 116 | # Remove elements from orset1 117 | self.orset1.remove('b') 118 | 119 | # Remove elements from orset2 120 | self.orset2.remove('b') 121 | self.orset2.remove('c') 122 | 123 | # Check orset1 merging 124 | self.orset1.merge(self.orset2) 125 | self.assertEqual([_['elem'] for _ in self.orset1.A], ['a', 'b', 'c', 'd']) 126 | self.assertEqual([_['elem'] for _ in self.orset1.R], ['b', 'c']) 127 | for _ in self.orset1.R: 128 | if _['elem'] == 'b': 129 | self.assertEqual(len(_['tags']), 2) 130 | break 131 | 132 | # Check orset2 merging 133 | self.orset2.merge(self.orset1) 134 | self.assertEqual([_['elem'] for _ in self.orset2.A], ['a', 'b', 'c', 'd']) 135 | self.assertEqual([_['elem'] for _ in self.orset2.R], ['b', 'c']) 136 | for _ in self.orset2.R: 137 | if _['elem'] == 'b': 138 | self.assertEqual(len(_['tags']), 2) 139 | break 140 | 141 | # Check if they are both equal 142 | self.assertEqual([_['elem'] for _ in self.orset1.A], [_['elem'] for _ in self.orset2.A]) 143 | self.assertEqual([_['elem'] for _ in self.orset1.R], [_['elem'] for _ in self.orset2.R]) 144 | 145 | def test_querying_orset_with_merging_with_removal(self): 146 | # Remove elements from orset1 147 | self.orset1.remove('b') 148 | 149 | # Remove elements from orset2 150 | self.orset2.remove('b') 151 | self.orset2.remove('c') 152 | 153 | # Merge orset2 to orset1 154 | self.orset1.merge(self.orset2) 155 | 156 | # Merge orset1 to orset2 157 | self.orset2.merge(self.orset1) 158 | 159 | # Check orset1 querying 160 | self.assertTrue(self.orset1.query('a')) 161 | self.assertFalse(self.orset1.query('b')) 162 | self.assertFalse(self.orset1.query('c')) 163 | self.assertTrue(self.orset1.query('d')) 164 | 165 | # Check orset2 querying 166 | self.assertTrue(self.orset2.query('a')) 167 | self.assertFalse(self.orset2.query('b')) 168 | self.assertFalse(self.orset2.query('c')) 169 | self.assertTrue(self.orset2.query('d')) 170 | 171 | 172 | if __name__ == '__main__': 173 | unittest.main() 174 | -------------------------------------------------------------------------------- /tests/test_pncounter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import uuid 3 | import py3crdt 4 | from py3crdt.pncounter import PNCounter 5 | from py3crdt.node import Node 6 | 7 | 8 | class TestPNCounter(unittest.TestCase): 9 | def setUp(self): 10 | # Create nodes 11 | self.node1 = Node(uuid.uuid4()) 12 | self.node2 = Node(uuid.uuid4()) 13 | 14 | # Create a PNCounter 15 | self.pn1 = PNCounter(uuid.uuid4()) 16 | 17 | # Add nodes to pn1 18 | self.pn1.add_new_node(self.node1.id) 19 | self.pn1.add_new_node(self.node2.id) 20 | 21 | # Increment pn1 values for each node 22 | self.pn1.inc(self.node1.id) 23 | self.pn1.inc(self.node1.id) 24 | self.pn1.inc(self.node1.id) 25 | self.pn1.inc(self.node1.id) 26 | self.pn1.inc(self.node2.id) 27 | self.pn1.inc(self.node2.id) 28 | self.pn1.inc(self.node2.id) 29 | 30 | # Decrement pn1 values for each node 31 | self.pn1.dec(self.node1.id) 32 | self.pn1.dec(self.node1.id) 33 | self.pn1.dec(self.node1.id) 34 | self.pn1.dec(self.node2.id) 35 | 36 | # Create another PNCounter 37 | self.pn2 = PNCounter(uuid.uuid4()) 38 | 39 | # Add nodes to pn2 40 | self.pn2.add_new_node(self.node1.id) 41 | self.pn2.add_new_node(self.node2.id) 42 | 43 | # Increment pn2 values for each node 44 | self.pn2.inc(self.node1.id) 45 | self.pn2.inc(self.node2.id) 46 | self.pn2.inc(self.node2.id) 47 | self.pn2.inc(self.node2.id) 48 | 49 | # Decrement self.pn2 values for each node 50 | self.pn2.dec(self.node1.id) 51 | self.pn2.dec(self.node2.id) 52 | self.pn2.dec(self.node2.id) 53 | 54 | def test_merge(self): 55 | # Merge pn2 with pn1 56 | self.pn2.merge(self.pn1) 57 | # Merge pn1 with pn2 58 | self.pn1.merge(self.pn2) 59 | 60 | self.assertEqual(self.pn1.query(), self.pn2.query()) 61 | 62 | 63 | if __name__ == '__main__': 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /tests/test_sequence.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import uuid 3 | import py3crdt 4 | from py3crdt.sequence import Sequence 5 | from datetime import datetime 6 | 7 | 8 | class TestSequence(unittest.TestCase): 9 | def setUp(self): 10 | # Create a Sequence 11 | self.seq1 = Sequence(uuid.uuid4()) 12 | 13 | # Create another Sequence 14 | self.seq2 = Sequence(uuid.uuid4()) 15 | 16 | # Add elements to seq1 17 | self.id1a = datetime.timestamp(datetime.now()) 18 | self.seq1.add('a', self.id1a) 19 | self.id1b = datetime.timestamp(datetime.now()) 20 | self.seq1.add('b', self.id1b) 21 | 22 | # Add elements to seq2 23 | self.id2c = datetime.timestamp(datetime.now()) 24 | self.seq2.add('c', self.id2c) 25 | self.id2b = datetime.timestamp(datetime.now()) 26 | self.seq2.add('b', self.id2b) 27 | self.id2d = datetime.timestamp(datetime.now()) 28 | self.seq2.add('d', self.id2d) 29 | 30 | def test_elements_add_correctly_sequence(self): 31 | self.assertEqual(self.seq1.get_seq(), "ab") 32 | self.assertEqual(self.seq2.get_seq(), "cbd") 33 | 34 | def test_querying_sequence_without_removal_and_merging(self): 35 | # Check seq1 querying 36 | self.assertTrue(self.seq1.query(self.id1a)) 37 | self.assertTrue(self.seq1.query(self.id1b)) 38 | self.assertFalse(self.seq1.query(self.id2b)) 39 | self.assertFalse(self.seq1.query(self.id2c)) 40 | self.assertFalse(self.seq1.query(self.id2d)) 41 | 42 | # Check seq2 querying 43 | self.assertFalse(self.seq2.query(self.id1a)) 44 | self.assertFalse(self.seq2.query(self.id1b)) 45 | self.assertTrue(self.seq2.query(self.id2c)) 46 | self.assertTrue(self.seq2.query(self.id2b)) 47 | self.assertTrue(self.seq2.query(self.id2d)) 48 | 49 | def test_merging_sequence_without_removal(self): 50 | # Check seq1 merging 51 | self.seq1.merge(self.seq2) 52 | self.assertEqual(self.seq1.get_seq(), "abcbd") 53 | 54 | # Check seq2 merging 55 | self.seq2.merge(self.seq1) 56 | self.assertEqual(self.seq2.get_seq(), "abcbd") 57 | 58 | # Check if they are both equal 59 | self.assertEqual(self.seq1.get_seq(), self.seq2.get_seq()) 60 | 61 | def test_querying_sequence_with_merging_without_removal(self): 62 | # Check seq2 merging 63 | self.seq2.merge(self.seq1) 64 | self.assertTrue(self.seq2.query(self.id1a)) 65 | self.assertTrue(self.seq2.query(self.id1b)) 66 | self.assertTrue(self.seq2.query(self.id2c)) 67 | self.assertTrue(self.seq2.query(self.id2b)) 68 | self.assertTrue(self.seq2.query(self.id2d)) 69 | 70 | # Check seq1 merging 71 | self.seq1.merge(self.seq2) 72 | self.assertTrue(self.seq1.query(self.id1a)) 73 | self.assertTrue(self.seq1.query(self.id1b)) 74 | self.assertTrue(self.seq1.query(self.id2b)) 75 | self.assertTrue(self.seq1.query(self.id2c)) 76 | self.assertTrue(self.seq1.query(self.id2d)) 77 | 78 | def test_elements_remove_correctly_sequence(self): 79 | # Remove elements from seq1 80 | self.seq1.remove(self.id1b) 81 | 82 | self.assertEqual(self.seq1.get_seq(), "a") 83 | 84 | # Remove elements from seq2 85 | self.seq2.remove(self.id2b) 86 | self.seq2.remove(self.id2c) 87 | 88 | self.assertEqual(self.seq2.get_seq(), "d") 89 | 90 | def test_querying_sequence_without_merging_with_removal(self): 91 | # Remove elements from seq1 92 | self.seq1.remove(self.id1b) 93 | 94 | # Check seq1 querying 95 | self.assertTrue(self.seq1.query(self.id1a)) 96 | self.assertFalse(self.seq1.query(self.id1b)) 97 | self.assertFalse(self.seq1.query(self.id2b)) 98 | self.assertFalse(self.seq1.query(self.id2c)) 99 | self.assertFalse(self.seq1.query(self.id2d)) 100 | 101 | # Remove elements from seq2 102 | self.seq2.remove(self.id2b) 103 | self.seq2.remove(self.id2c) 104 | 105 | # Check seq2 querying 106 | self.assertFalse(self.seq2.query(self.id1a)) 107 | self.assertFalse(self.seq2.query(self.id1b)) 108 | self.assertFalse(self.seq2.query(self.id2b)) 109 | self.assertFalse(self.seq2.query(self.id2c)) 110 | self.assertTrue(self.seq2.query(self.id2d)) 111 | 112 | def test_merging_sequence_with_removal(self): 113 | # Remove elements from seq1 114 | self.seq1.remove(self.id1b) 115 | 116 | # Remove elements from seq2 117 | self.seq2.remove(self.id2c) 118 | 119 | # Check seq1 merging 120 | self.seq1.merge(self.seq2) 121 | self.assertEqual(self.seq1.get_seq(), "abd") 122 | 123 | # Check seq2 merging 124 | self.seq2.merge(self.seq1) 125 | self.assertEqual(self.seq2.get_seq(), "abd") 126 | 127 | # Check if they are both equal 128 | self.assertEqual(self.seq2.get_seq(), self.seq2.get_seq()) 129 | 130 | def test_querying_sequence_with_merging_with_removal(self): 131 | # Remove elements from seq1 132 | self.seq1.remove(self.id1b) 133 | 134 | # Remove elements from seq2 135 | self.seq2.remove(self.id2c) 136 | 137 | # Merge seq2 to seq1 138 | self.seq1.merge(self.seq2) 139 | 140 | # Merge seq1 to seq2 141 | self.seq2.merge(self.seq1) 142 | 143 | # Check seq1 querying 144 | self.assertTrue(self.seq1.query(self.id1a)) 145 | self.assertFalse(self.seq1.query(self.id1b)) 146 | self.assertTrue(self.seq1.query(self.id2b)) 147 | self.assertFalse(self.seq1.query(self.id2c)) 148 | self.assertTrue(self.seq1.query(self.id2d)) 149 | 150 | # Check seq2 querying 151 | self.assertTrue(self.seq2.query(self.id1a)) 152 | self.assertFalse(self.seq2.query(self.id1b)) 153 | self.assertTrue(self.seq2.query(self.id2b)) 154 | self.assertFalse(self.seq2.query(self.id2c)) 155 | self.assertTrue(self.seq2.query(self.id2d)) 156 | 157 | 158 | if __name__ == '__main__': 159 | unittest.main() 160 | -------------------------------------------------------------------------------- /tests/test_twopset.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import uuid 3 | import py3crdt 4 | from py3crdt.twopset import TwoPSet 5 | 6 | 7 | class TestLWW(unittest.TestCase): 8 | def setUp(self): 9 | # Create a TwoPSet 10 | self.twopset1 = TwoPSet(uuid.uuid4()) 11 | 12 | # Create another TwoPSet 13 | self.twopset2 = TwoPSet(uuid.uuid4()) 14 | 15 | # Add elements to twopset1 16 | self.twopset1.add('a') 17 | self.twopset1.add('b') 18 | 19 | # Add elements to twopset1 20 | self.twopset2.add('b') 21 | self.twopset2.add('c') 22 | self.twopset2.add('d') 23 | 24 | def test_elements_add_correctly_twopset(self): 25 | self.assertEqual(self.twopset1.A.payload, ['a', 'b']) 26 | self.assertEqual(self.twopset1.R.payload, []) 27 | self.assertEqual(self.twopset2.A.payload, ['b', 'c', 'd']) 28 | self.assertEqual(self.twopset2.R.payload, []) 29 | 30 | def test_querying_twopset_without_removal_and_merging(self): 31 | # Check twopset1 querying 32 | self.assertTrue(self.twopset1.query('a')) 33 | self.assertTrue(self.twopset1.query('b')) 34 | self.assertFalse(self.twopset1.query('c')) 35 | self.assertFalse(self.twopset1.query('d')) 36 | 37 | # Check twopset2 querying 38 | self.assertFalse(self.twopset2.query('a')) 39 | self.assertTrue(self.twopset2.query('b')) 40 | self.assertTrue(self.twopset2.query('c')) 41 | self.assertTrue(self.twopset2.query('d')) 42 | 43 | def test_merging_twopset_without_removal(self): 44 | # Check twopset1 merging 45 | self.twopset1.merge(self.twopset2) 46 | self.assertEqual(self.twopset1.A.payload, ['a', 'b', 'c', 'd']) 47 | self.assertEqual(self.twopset1.R.payload, []) 48 | 49 | # Check twopset2 merging 50 | self.twopset2.merge(self.twopset1) 51 | self.assertEqual(self.twopset2.A.payload, ['a', 'b', 'c', 'd']) 52 | self.assertEqual(self.twopset2.R.payload, []) 53 | 54 | # Check if they are both equal 55 | self.assertEqual(self.twopset1.A.payload, self.twopset2.A.payload) 56 | self.assertEqual(self.twopset1.R.payload, self.twopset2.R.payload) 57 | 58 | def test_querying_twopset_with_merging_without_removal(self): 59 | # Check twopset2 merging 60 | self.twopset2.merge(self.twopset1) 61 | self.assertTrue(self.twopset2.query('a')) 62 | self.assertTrue(self.twopset2.query('b')) 63 | self.assertTrue(self.twopset2.query('c')) 64 | self.assertTrue(self.twopset2.query('d')) 65 | 66 | # Check twopset1 merging 67 | self.twopset1.merge(self.twopset2) 68 | self.assertTrue(self.twopset1.query('a')) 69 | self.assertTrue(self.twopset1.query('b')) 70 | self.assertTrue(self.twopset1.query('c')) 71 | self.assertTrue(self.twopset1.query('d')) 72 | 73 | def test_elements_remove_correctly_twopset(self): 74 | # Remove elements from twopset1 75 | self.twopset1.remove('b') 76 | 77 | self.assertEqual(self.twopset1.A.payload, ['a', 'b']) 78 | self.assertEqual(self.twopset1.R.payload, ['b']) 79 | 80 | # Remove elements from twopset2 81 | self.twopset2.remove('b') 82 | self.twopset2.remove('c') 83 | 84 | self.assertEqual(self.twopset2.A.payload, ['b', 'c', 'd']) 85 | self.assertEqual(self.twopset2.R.payload, ['b', 'c']) 86 | 87 | def test_querying_twopset_without_merging_with_removal(self): 88 | # Remove elements from twopset1 89 | self.twopset1.remove('b') 90 | 91 | # Check twopset1 querying 92 | self.assertTrue(self.twopset1.query('a')) 93 | self.assertFalse(self.twopset1.query('b')) 94 | self.assertFalse(self.twopset1.query('c')) 95 | self.assertFalse(self.twopset1.query('d')) 96 | 97 | # Remove elements from twopset2 98 | self.twopset2.remove('b') 99 | self.twopset2.remove('c') 100 | 101 | # Check twopset2 querying 102 | self.assertFalse(self.twopset2.query('a')) 103 | self.assertFalse(self.twopset2.query('b')) 104 | self.assertFalse(self.twopset2.query('c')) 105 | self.assertTrue(self.twopset2.query('d')) 106 | 107 | def test_merging_twopset_with_removal(self): 108 | # Remove elements from twopset1 109 | self.twopset1.remove('b') 110 | 111 | # Remove elements from twopset2 112 | self.twopset2.remove('b') 113 | self.twopset2.remove('c') 114 | 115 | # Check twopset1 merging 116 | self.twopset1.merge(self.twopset2) 117 | self.assertEqual(self.twopset1.A.payload, ['a', 'b', 'c', 'd']) 118 | self.assertEqual(self.twopset1.R.payload, ['b', 'c']) 119 | 120 | # Check twopset2 merging 121 | self.twopset2.merge(self.twopset1) 122 | self.assertEqual(self.twopset2.A.payload, ['a', 'b', 'c', 'd']) 123 | self.assertEqual(self.twopset2.R.payload, ['b', 'c']) 124 | 125 | # Check if they are both equal 126 | self.assertEqual(self.twopset1.A.payload, self.twopset2.A.payload) 127 | self.assertEqual(self.twopset1.R.payload, self.twopset2.R.payload) 128 | 129 | def test_querying_twopset_with_merging_with_removal(self): 130 | # Remove elements from twopset1 131 | self.twopset1.remove('b') 132 | 133 | # Remove elements from twopset2 134 | self.twopset2.remove('b') 135 | self.twopset2.remove('c') 136 | 137 | # Merge twopset2 to twopset1 138 | self.twopset1.merge(self.twopset2) 139 | 140 | # Merge twopset1 to twopset2 141 | self.twopset2.merge(self.twopset1) 142 | 143 | # Check twopset1 querying 144 | self.assertTrue(self.twopset1.query('a')) 145 | self.assertFalse(self.twopset1.query('b')) 146 | self.assertFalse(self.twopset1.query('c')) 147 | self.assertTrue(self.twopset1.query('d')) 148 | 149 | # Check twopset2 querying 150 | self.assertTrue(self.twopset2.query('a')) 151 | self.assertFalse(self.twopset2.query('b')) 152 | self.assertFalse(self.twopset2.query('c')) 153 | self.assertTrue(self.twopset2.query('d')) 154 | 155 | 156 | if __name__ == '__main__': 157 | unittest.main() 158 | --------------------------------------------------------------------------------