From 167af1af6276a4b58ae8c998e95cbdc3a47740bf Mon Sep 17 00:00:00 2001 From: Fabian Utech Date: Fri, 27 Oct 2023 16:34:59 +0200 Subject: [PATCH 01/19] Added overview of BDC features for issue #17 Overview over possible features we want to add to our Base Data Collector (BDC) --- Documentation/Media/BDC_Features.drawio | 215 ++++++++++++++++++ .../Media/BDC_Features.drawio.license | 6 + 2 files changed, 221 insertions(+) create mode 100644 Documentation/Media/BDC_Features.drawio create mode 100644 Documentation/Media/BDC_Features.drawio.license diff --git a/Documentation/Media/BDC_Features.drawio b/Documentation/Media/BDC_Features.drawio new file mode 100644 index 0000000..b109f91 --- /dev/null +++ b/Documentation/Media/BDC_Features.drawio @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Documentation/Media/BDC_Features.drawio.license b/Documentation/Media/BDC_Features.drawio.license new file mode 100644 index 0000000..dbe713d --- /dev/null +++ b/Documentation/Media/BDC_Features.drawio.license @@ -0,0 +1,6 @@ +SPDX-License-Identifier: MIT +SPDX-FileCopyrightText: 2023 Lucca Baumgärtner +SPDX-FileCopyrightText: 2023 Sophie Heasman +SPDX-FileCopyrightText: 2023 Tetiana Kraft +SPDX-FileCopyrightText: 2023 Ruchita Nathani +SPDX-FileCopyrightText: 2023 Fabian-Paul Utech From a0039d96cf70208418974a687d33e699ef2b1f5a Mon Sep 17 00:00:00 2001 From: Berkay Bozkurt Date: Fri, 3 Nov 2023 18:02:43 +0100 Subject: [PATCH 02/19] Controller created #36 Signed-off-by: Berkay Bozkurt --- src/controller/Controller.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/controller/Controller.py diff --git a/src/controller/Controller.py b/src/controller/Controller.py new file mode 100644 index 0000000..980aab0 --- /dev/null +++ b/src/controller/Controller.py @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Berkay Bozkurt + +from threading import Lock +from typing import Any + + +class ControllerMeta(type): + + """ + Thread safe singleton implementation of Controller + """ + + _instances = {} + _lock: Lock = Lock() + + def __call__(self, *args: Any, **kwds: Any): + with self._lock: + if self not in self._instances: + instance = super().__call__(*args, **kwds) + self._instances[self] = instance + + return self._instances[self] + + +class Controller(metaclass=ControllerMeta): + def __init__(self, name: str) -> None: + self.name = name From a5d2345bebc48623dae05476b6c765b5200aa85d Mon Sep 17 00:00:00 2001 From: Felix Zailskas Date: Thu, 26 Oct 2023 16:58:53 +0200 Subject: [PATCH 03/19] Created a small skeleton on how the EVP might be interacted with. Added some dummy data for example purposes. #22 Signed-off-by: Felix Zailskas --- LICENSES/CC-BY-4.0.txt | 156 ++++++++++++++++++++++++++ Pipfile | 2 + src/database/__init__.py | 13 +++ src/database/database_dummy.py | 17 +++ src/database/dummy_leads.json | 59 ++++++++++ src/database/dummy_leads.json.license | 2 + src/evp/__init__.py | 2 + src/evp/evp.py | 53 +++++++++ src/evp_demo.py | 26 +++++ 9 files changed, 330 insertions(+) create mode 100644 LICENSES/CC-BY-4.0.txt create mode 100644 src/database/__init__.py create mode 100644 src/database/database_dummy.py create mode 100644 src/database/dummy_leads.json create mode 100644 src/database/dummy_leads.json.license create mode 100644 src/evp/__init__.py create mode 100644 src/evp/evp.py create mode 100644 src/evp_demo.py diff --git a/LICENSES/CC-BY-4.0.txt b/LICENSES/CC-BY-4.0.txt new file mode 100644 index 0000000..13ca539 --- /dev/null +++ b/LICENSES/CC-BY-4.0.txt @@ -0,0 +1,156 @@ +Creative Commons Attribution 4.0 International + + Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. + +Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. + +Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. + +Creative Commons Attribution 4.0 International Public License + +By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. + +Section 1 – Definitions. + + a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. + + c. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. + + d. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. + + e. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. + + f. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. + + g. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. + + h. Licensor means the individual(s) or entity(ies) granting rights under this Public License. + + i. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. + + j. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. + + k. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. + +Section 2 – Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: + + A. reproduce and Share the Licensed Material, in whole or in part; and + + B. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. + + 3. Term. The term of this Public License is specified in Section 6(a). + + 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. + + 5. Downstream recipients. + + A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. + + B. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. + + 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). + +b. Other rights. + + 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this Public License. + + 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. + +Section 3 – License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified form), You must: + + A. retain the following if it is supplied by the Licensor with the Licensed Material: + + i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of warranties; + + v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; + + B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and + + C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. + + 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. + + 4. If You Share Adapted Material You produce, the Adapter's License You apply must not prevent recipients of the Adapted Material from complying with this Public License. + +Section 4 – Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; + + b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material; and + + c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. +For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. + +Section 5 – Disclaimer of Warranties and Limitation of Liability. + + a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. + + b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. + + c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. + +Section 6 – Term and Termination. + + a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or + + 2. upon express reinstatement by the Licensor. + + c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. + + d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. + + e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. + +Section 7 – Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. + +Section 8 – Interpretation. + + a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. + + c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. + + d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. + +Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/Pipfile b/Pipfile index 0f71bda..a8cec40 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,8 @@ name = "pypi" [dev-packages] [packages] +numpy = "==1.26.1" +scikit-learn = "==1.3.2" [requires] python_version = "3.10" diff --git a/src/database/__init__.py b/src/database/__init__.py new file mode 100644 index 0000000..8d5e73b --- /dev/null +++ b/src/database/__init__.py @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Felix Zailskas + +from .database_dummy import DatabaseDummy + +_database = None + + +def get_database() -> DatabaseDummy: + global _database + if _database is None: + _database = DatabaseDummy() + return _database diff --git a/src/database/database_dummy.py b/src/database/database_dummy.py new file mode 100644 index 0000000..fd8a5c0 --- /dev/null +++ b/src/database/database_dummy.py @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Felix Zailskas + +import json + + +class DatabaseDummy: + def __init__(self) -> None: + with open("src/database/dummy_leads.json") as f: + json_data = json.load(f)["training_leads"] + self.data = {d["lead_id"]: d for d in json_data} + + def get_entry_by_id(self, id_: int) -> dict: + return self.data[id_] + + def get_all_entries(self): + return self.data diff --git a/src/database/dummy_leads.json b/src/database/dummy_leads.json new file mode 100644 index 0000000..be73afc --- /dev/null +++ b/src/database/dummy_leads.json @@ -0,0 +1,59 @@ +{ + "training_leads": [ + { + "lead_id": 0, + "company_name": "test_company", + "first_name": "test_first", + "last_name": "test_last", + "country_code": "DE", + "phone_number": 176123123, + "email_address": "test@test.de", + "customer_probability": 0.1, + "life_time_value": 400000 + }, + { + "lead_id": 1, + "company_name": "test_company", + "first_name": "test_first", + "last_name": "test_last", + "country_code": "DE", + "phone_number": 176123123, + "email_address": "test@test.de", + "customer_probability": 0.9, + "life_time_value": 1000 + }, + { + "lead_id": 2, + "company_name": "test_company", + "first_name": "test_first", + "last_name": "test_last", + "country_code": "DE", + "phone_number": 176123123, + "email_address": "test@test.de", + "customer_probability": 0.7, + "life_time_value": 3500 + }, + { + "lead_id": 3, + "company_name": "test_company", + "first_name": "test_first", + "last_name": "test_last", + "country_code": "DE", + "phone_number": 176123123, + "email_address": "test@test.de", + "customer_probability": 0.4, + "life_time_value": 10000 + }, + { + "lead_id": 4, + "company_name": "test_company", + "first_name": "test_first", + "last_name": "test_last", + "country_code": "DE", + "phone_number": 176123123, + "email_address": "test@test.de", + "customer_probability": 0.32, + "life_time_value": 20000 + } + ] +} diff --git a/src/database/dummy_leads.json.license b/src/database/dummy_leads.json.license new file mode 100644 index 0000000..55f896a --- /dev/null +++ b/src/database/dummy_leads.json.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC-BY-4.0 +SPDX-FileCopyrightText: 2023 Felix Zailskas diff --git a/src/evp/__init__.py b/src/evp/__init__.py new file mode 100644 index 0000000..5a15f67 --- /dev/null +++ b/src/evp/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Felix Zailskas diff --git a/src/evp/evp.py b/src/evp/evp.py new file mode 100644 index 0000000..9d43698 --- /dev/null +++ b/src/evp/evp.py @@ -0,0 +1,53 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Felix Zailskas + +import numpy as np +from sklearn.linear_model import LinearRegression + +from database import get_database + + +class LeadValue: + def __init__( + self, lifetime_value: float = 0, customer_probability: float = 0 + ) -> None: + assert ( + 0.0 <= customer_probability <= 1.0 + ), "Probability of becoming a customer must be between 0.0 and 1.0" + self.life_time_value = lifetime_value + self.customer_probability = customer_probability + + def get_lead_value(self) -> float: + return self.life_time_value * self.customer_probability + + +class EstimatedValuePredictor: + def __init__(self) -> None: + self.probability_predictor = LinearRegression() + self.life_time_value_predictor = LinearRegression() + + data = get_database().get_all_entries() + X = np.random.random((len(data), len(data))) + y_probability = np.array( + [item["customer_probability"] for item in data.values()] + ) + y_value = np.array([item["customer_probability"] for item in data.values()]) + + self.probability_predictor.fit(X, y_probability) + self.life_time_value_predictor.fit(X, y_value) + + def estimate_value(self, lead_id) -> LeadValue: + # make call to data base to retrieve relevant fields for this lead + lead_data = get_database().get_entry_by_id(lead_id) + + # preprocess lead_data to get feature vector for our ML model + feature_vector = np.random.random((1, 5)) + + # use the models to predict required values + lead_value_pred = self.life_time_value_predictor.predict(feature_vector) + # manually applying sigmoid to ensure value in range 0, 1 + cust_prob_pred = 1 / ( + 1 + np.exp(-self.probability_predictor.predict(feature_vector)) + ) + + return LeadValue(lead_value_pred, cust_prob_pred) diff --git a/src/evp_demo.py b/src/evp_demo.py new file mode 100644 index 0000000..5179001 --- /dev/null +++ b/src/evp_demo.py @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Felix Zailskas + +from database import get_database +from evp.evp import EstimatedValuePredictor + +lead_id = 0 + +lead_data = get_database().get_entry_by_id(lead_id) + +evp = EstimatedValuePredictor() +lead_value = evp.estimate_value(lead_id) + +print( + f""" + Dummy prediction for {lead_id=}: + + Data: + {lead_data} + + This lead has a predicted probability of {lead_value.customer_probability} to become a customer. + This lead has a predicted life time value of {lead_value.life_time_value}. + + This results in a total lead value of {lead_value.get_lead_value()}. +""" +) From 966ff58267675bb18cd21514f1409d27b6ba281c Mon Sep 17 00:00:00 2001 From: Ahmed Sheta Date: Thu, 26 Oct 2023 15:31:51 +0200 Subject: [PATCH 04/19] added the documentation for pull requests in file Contribution.md Signed-off-by: Ahmed Sheta --- Deliverables/README.md | 10 ---- Documentation/Contribution.md | 92 +++++++++++++++++++++++++++++++++++ Documentation/README.md | 7 --- 3 files changed, 92 insertions(+), 17 deletions(-) delete mode 100644 Deliverables/README.md create mode 100644 Documentation/Contribution.md delete mode 100644 Documentation/README.md diff --git a/Deliverables/README.md b/Deliverables/README.md deleted file mode 100644 index b5e105e..0000000 --- a/Deliverables/README.md +++ /dev/null @@ -1,10 +0,0 @@ - - -Deliverables that are uploadable files (e.g. PDFs, your logo, but not your code) go here. - -Please upload deliverables to a dedicated folder for the sprint they belong to. - -Please prefix all files with the sprint they belong to, as explained in the deliverables instructions. diff --git a/Documentation/Contribution.md b/Documentation/Contribution.md new file mode 100644 index 0000000..1b0316d --- /dev/null +++ b/Documentation/Contribution.md @@ -0,0 +1,92 @@ + + +# Contribution Workflow + +## Branching Strategy + +**main**: It contains fully stable production code + +- **dev**: It contains stable under-development code + + - **epic**: It contains a module branch. Like high level of feature. For example, we have an authentication module then we can create a branch like "epic/authentication" + + - **feature**: It contains specific features under the module. For example, under authentication, we have a feature called registration. Sample branch name: "feature/registration" + + - **bugfix**: It contains bug fixing during the testing phase and branch name start with the issue number for example "bugfix/3-validate-for-wrong-user-name" + +## Commits and Pull Requests + +The stable branches `main` and `dev` are protected against direct pushes. To commit code to these branches create a pull request (PR) describing the feature/bugfix that you are committing to the `dev` branch. This PR will then be reviewed by another SD from the project. Only after being approved by another SD a PR may be merged into the `dev` branch. Periodically the stable code on the `dev` branch will be merged into the `main` branch by creating a PR from `dev`. Hence, every feature that should be committed to the `main` branch must first run without issues on the `dev` branch for some time. + +Before contributing to this repository make sure that you are identifiable in your git user settings. This way commits and PRs created by you can be identified and easily traced back. + +```[bash] +git config --local user.name "Manu Musterperson" +git config --local user.email "manu@musterperson.org" +``` + +Any commit should always contain a commit message that references an issue created in the project. Also, always signoff on your commits for identification reasons. + +```[bash] +git commit -m "Fixed issue #123" --signoff +``` + +When doing pair programming be sure to always have all SDs mentioned in the commit message. Each SD should be listed on a new line for clarity reasons. + +```[bash] +git commit -a -m "Fixed problem #123 +> Co-authored-by: Manu Musterperson " --signoff +``` + +## Pull Request Workflow + +The **main** and **dev** branches are protected against direct pushes, which means that we want to do a Pull Request (PR) in order to merge a developed branch into these branches. Having developed a branch (let's call it **feature-1**) and we want to merge **feature-1** branch into **main** branch. + +Here is a standard way to merge pull requests: + +1. Have all your local changes added, committed, and pushed on the remote **feature-1** branch + + ```[bash] + git checkout feature-1 + git add . + git commit -m "added a feature" --signoff # don't forget the signoff ;) + git push + ``` + +2. Make sure your local main branch up-to-date + + ```[bash] + git checkout main + git pull main + ``` + +3. Go to [Pull Requests](https://github.com/amosproj/amos2023ws06-sales-lead-qualifier/pulls) > click on "New pull request" > make sure the base is **main** branch (or **dev** branch, depends on which branch you want to update) and the compare to be your **feature-1** branch, as highlighted in the photo below and click "create pull requests": + ![image](https://github.com/amosproj/amos2023ws06-sales-lead-qualifier/assets/95130956/3eceb19d-bdb7-41bd-aa58-07ed7fb6148a) + + Make sure to link the issue your PR relates to. + +4. Inform the other SDs on slack that you have created the PR and it is awaiting a review and wait for others to review your code. The reviewers will potentially leave comments and change requests in their PR review. If this is the case reason why the change request is not warranted or checkout your branch again and apply the requested changes. Then push your branch once more and request another review by the reviewer. Once there are no more change requests and the PR has been approved by another SD you can merge the PR into the target branch. + +5. Delete the feature branch **feature-1** once it has been merged into the target branch. + +_**In case of merge conflict:**_ + +Should we experience merge conflict after step 3, we should solve the merge conflicts manually, below the title of "This branch has conflicts that must be resolved" click on web editor (you can use vscode or any editor you want). +The conflict should look like this: + +```[bash] +<<<<<<< HEAD +// Your changes at **feature-1** branch +======= +// Data already on the main branch +>>>>>>> main +``` + +-choose which one of these you would adopt for the merge to the **main** branch, we would be better off solving the merge -conflicts together rather than alone, feel free to announce it in the slack group chat. +-mark it as resolved and remerge the PR again, there shouldn't any problem with it. + +Feel free to add more about that matter here. \ No newline at end of file diff --git a/Documentation/README.md b/Documentation/README.md deleted file mode 100644 index f2b6606..0000000 --- a/Documentation/README.md +++ /dev/null @@ -1,7 +0,0 @@ - - -Build, user, and technical documentation -Software architecture description From 0446054075a27356f2dbe8f5bf36b7bc4c17f7c7 Mon Sep 17 00:00:00 2001 From: Ahmed Sheta Date: Thu, 26 Oct 2023 16:00:54 +0200 Subject: [PATCH 05/19] modified the main README file Signed-off-by: Ahmed Sheta --- README.md | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/README.md b/README.md index 1c4fabc..2fdb2a1 100644 --- a/README.md +++ b/README.md @@ -47,44 +47,6 @@ Note that this project runs under an MIT license and we only permit the use of n When you have any issues with the environment contact `felix-zailskas`. -## Contribution Workflow - -### Branching Strategy - -**main**: It contains fully stable production code - -- **dev**: It contains stable under-development code - - - **epic**: It contains a module branch. Like high level of feature. For example, we have an authentication module then we can create a branch like "epic/authentication" - - - **feature**: It contains specific features under the module. For example, under authentication, we have a feature called registration. Sample branch name: "feature/registration" - - - **bugfix**: It contains bug fixing during the testing phase and branch name start with the issue number for example "bugfix/3-validate-for-wrong-user-name" - -### Commits and Pull Requests - -The stable branches `main` and `dev` are protected against direct pushes. To commit code to these branches create a pull request (PR) describing the feature/bugfix that you are committing to the `dev` branch. This PR will then be reviewed by another SD from the project. Only after being approved by another SD a PR may be merged into the `dev` branch. Periodically the stable code on the `dev` branch will be merged into the `main` branch by creating a PR from `dev`. Hence, every feature that should be committed to the `main` branch must first run without issues on the `dev` branch for some time. - -Before contributing to this repository make sure that you are identifiable in your git user settings. This way commits and PRs created by you can be identified and easily traced back. - -```[bash] -git config --local user.name "Manu Musterperson" -git config --local user.email "manu@musterperson.org" -``` - -Any commit should always contain a commit message that references an issue created in the project. Also, always signoff on your commits for identification reasons. - -```[bash] -git commit -m "Fixed issue #123" --signoff -``` - -When doing pair programming be sure to always have all SDs mentioned in the commit message. Each SD should be listed on a new line for clarity reasons. - -```[bash] -git commit -a -m "Fixed problem #123 -> Co-authored-by: Manu Musterperson " -``` - ### License This project is operated under an MIT license. Every file must contain the REUSE-compliant license and copyright declaration: From dda15b4930bc8f3304232d51e2248effd5ec461c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucca=20Baumg=C3=A4rtner?= <44930425+luccalb@users.noreply.github.com> Date: Sat, 28 Oct 2023 17:36:22 +0200 Subject: [PATCH 06/19] Feature/9 initial cicd pipeline (#38) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --------- Signed-off-by: Lucca Baumgärtner --- .github/workflows/python-app.yml | 69 ++++++++++++++++++++++++++++++++ Pipfile | 2 + src/test_dummy.py | 8 ++++ 3 files changed, 79 insertions(+) create mode 100644 .github/workflows/python-app.yml create mode 100644 src/test_dummy.py diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..894bfd9 --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Lucca Baumgärtner + +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + +name: Python application + +on: [push, pull_request] + +permissions: + contents: read + +jobs: + license: + name: License check + runs-on: ubuntu-latest + steps: + - name: Checkout the code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Dump all dependencies + run: | + python -m pip install --upgrade pip + pip install pipenv + pipenv install + pipenv run pip freeze > requirements-all.txt + - name: Check copyright + id: license_check_report + uses: pilosus/action-pip-license-checker@v2 + with: + requirements: "requirements-all.txt" + fail: "Copyleft" + totals: true + headers: true + - name: Print copyright report + run: echo "${{ steps.license_check_report.outputs.report }}" + + build: + name: Lint and Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest pipenv + # if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pipenv install + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pipenv run pytest diff --git a/Pipfile b/Pipfile index a8cec40..d80e15d 100644 --- a/Pipfile +++ b/Pipfile @@ -7,10 +7,12 @@ verify_ssl = true name = "pypi" [dev-packages] +pytest = "==7.4.3" [packages] numpy = "==1.26.1" scikit-learn = "==1.3.2" +numpy = "==1.26.1" [requires] python_version = "3.10" diff --git a/src/test_dummy.py b/src/test_dummy.py new file mode 100644 index 0000000..fb7df7b --- /dev/null +++ b/src/test_dummy.py @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Lucca Baumgärtner +def inc(x): + return x + 1 + + +def test_inc(): + assert inc(2) == 3 From f946085da11a22fe1c22876cdf4965b73b88ed3d Mon Sep 17 00:00:00 2001 From: Tims777 Date: Fri, 27 Oct 2023 16:24:29 +0200 Subject: [PATCH 07/19] Fix duplicate logo in README Fix the logo so that only the correct version (dark / light) gets displayed. --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2fdb2a1..e128571 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,11 @@ SPDX-FileCopyrightText: 2023 Berkay Bozkurt ## Sum Insight Logo -![Team logo in white colour](https://github.com/amosproj/amos2023ws06-sales-lead-qualifier/assets/45459787/7d38795e-6bd5-4003-9b23-29911532b0f8#gh-dark-mode-only) - -![Team logo in black colour](https://github.com/amosproj/amos2023ws06-sales-lead-qualifier/assets/45459787/a7314df0-1917-4384-8f6c-2ab9f9831047#gh-light-mode-only) + + + + Sum Insights Logo + ## Creating the Environment From f148d178a161b85652f07e46bfecf9a4331996c3 Mon Sep 17 00:00:00 2001 From: Tims777 Date: Fri, 27 Oct 2023 16:39:11 +0200 Subject: [PATCH 08/19] Fine tuning The dynamic media source was still not working, but now it should --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e128571..e0e11a5 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ SPDX-FileCopyrightText: 2023 Berkay Bozkurt ## Sum Insight Logo - - + + Sum Insights Logo From 653af1ab01cc0b50246dcb69e2d7b18dfd3ee7ef Mon Sep 17 00:00:00 2001 From: Felix Zailskas Date: Thu, 2 Nov 2023 15:24:21 +0100 Subject: [PATCH 09/19] Adjusted lead dummies with new data field definitions. Added field validation to Lead object Signed-off-by: Felix Zailskas --- Pipfile | 3 +- src/database/database_dummy.py | 19 +++++++--- src/database/dummy_leads.json | 66 +++++++++++++++++----------------- src/database/models.py | 49 +++++++++++++++++++++++++ src/database/parsers.py | 43 ++++++++++++++++++++++ src/evp/evp.py | 36 ++++++++----------- src/evp_demo.py | 16 ++++----- {src => tests}/test_dummy.py | 0 8 files changed, 165 insertions(+), 67 deletions(-) create mode 100644 src/database/models.py create mode 100644 src/database/parsers.py rename {src => tests}/test_dummy.py (100%) diff --git a/Pipfile b/Pipfile index d80e15d..36e2718 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,8 @@ pytest = "==7.4.3" [packages] numpy = "==1.26.1" scikit-learn = "==1.3.2" -numpy = "==1.26.1" +pydantic = "==2.4.2" +email-validator = "==2.1.0" [requires] python_version = "3.10" diff --git a/src/database/database_dummy.py b/src/database/database_dummy.py index fd8a5c0..b361fac 100644 --- a/src/database/database_dummy.py +++ b/src/database/database_dummy.py @@ -2,6 +2,10 @@ # SPDX-FileCopyrightText: 2023 Felix Zailskas import json +from typing import List + +from database.models import Lead +from database.parsers import LeadParser class DatabaseDummy: @@ -10,8 +14,15 @@ def __init__(self) -> None: json_data = json.load(f)["training_leads"] self.data = {d["lead_id"]: d for d in json_data} - def get_entry_by_id(self, id_: int) -> dict: - return self.data[id_] + def get_lead_by_id(self, id_: int) -> Lead: + return LeadParser.parse_lead_from_dict(self.data[id_]) + + def get_all_leads(self) -> List[Lead]: + leads = [] + for entry in self.data.values(): + leads.append(LeadParser.parse_lead_from_dict(entry)) + return leads - def get_all_entries(self): - return self.data + def update_lead(self, lead: Lead): + print(f"Updating database entry for lead#{lead.lead_id}") + print(f"Update values: {lead}") diff --git a/src/database/dummy_leads.json b/src/database/dummy_leads.json index be73afc..e70b7e3 100644 --- a/src/database/dummy_leads.json +++ b/src/database/dummy_leads.json @@ -2,58 +2,58 @@ "training_leads": [ { "lead_id": 0, - "company_name": "test_company", - "first_name": "test_first", - "last_name": "test_last", - "country_code": "DE", - "phone_number": 176123123, + "annual_income": 25000, + "product_of_interest": "Terminals", + "first_name": "Anton", + "last_name": "Kerner", + "phone_number": "49176123123", "email_address": "test@test.de", "customer_probability": 0.1, "life_time_value": 400000 }, { "lead_id": 1, - "company_name": "test_company", - "first_name": "test_first", - "last_name": "test_last", - "country_code": "DE", - "phone_number": 176123123, + "annual_income": 70000, + "product_of_interest": "Terminals", + "first_name": "Anton", + "last_name": "Kerner", + "phone_number": "49176123123", "email_address": "test@test.de", - "customer_probability": 0.9, - "life_time_value": 1000 + "customer_probability": 0.4, + "life_time_value": 40000 }, { "lead_id": 2, - "company_name": "test_company", - "first_name": "test_first", - "last_name": "test_last", - "country_code": "DE", - "phone_number": 176123123, + "annual_income": 15000, + "product_of_interest": "Terminals", + "first_name": "Anton", + "last_name": "Kerner", + "phone_number": "49176123123", "email_address": "test@test.de", - "customer_probability": 0.7, - "life_time_value": 3500 + "customer_probability": 0.8, + "life_time_value": 40000 }, { "lead_id": 3, - "company_name": "test_company", - "first_name": "test_first", - "last_name": "test_last", - "country_code": "DE", - "phone_number": 176123123, + "annual_income": 2500000, + "product_of_interest": "Terminals", + "first_name": "Anton", + "last_name": "Kerner", + "phone_number": "49176123123", "email_address": "test@test.de", - "customer_probability": 0.4, - "life_time_value": 10000 + "customer_probability": 0.08, + "life_time_value": 400000 }, { "lead_id": 4, - "company_name": "test_company", - "first_name": "test_first", - "last_name": "test_last", - "country_code": "DE", - "phone_number": 176123123, + "annual_income": 1200, + "product_of_interest": "Terminals", + "first_name": "Anton", + "last_name": "Kerner", + "phone_number": "49176123123", "email_address": "test@test.de", - "customer_probability": 0.32, - "life_time_value": 20000 + "customer_probability": 0.9, + "life_time_value": 3400.23 } ] } diff --git a/src/database/models.py b/src/database/models.py new file mode 100644 index 0000000..2cc4a0a --- /dev/null +++ b/src/database/models.py @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Felix Zailskas + +from enum import Enum, IntEnum +from typing import List, Optional + +from pydantic import BaseModel, EmailStr, Field + + +class AnnualIncome(IntEnum): + Nothing = 0 # 0€ + Class1 = 1 # (0€, 35000€] + Class2 = 35001 # (35000€, 60000€] + Class3 = 60001 # (60000€, 100000€] + Class4 = 100001 # (100000€, 200000€] + Class5 = 200001 # (200000€, 400000€] + Class6 = 400001 # (400000€, 600000€] + Class7 = 600001 # (600000€, 1000000€] + Class8 = 1000001 # (1000000€, 2000000€] + Class9 = 2000001 # (2000000€, 5000000€] + Class10 = 5000001 # (5000000€, inf€] + + +class ProductOfInterest(str, Enum): + Nothing = "Nothing" + Terminals = "Terminals" + CashRegisterSystem = "Cash Register System" + BusinessAccount = "Business Account" + All = "All" + Other = "Other" + + +class LeadValue(BaseModel): + life_time_value: float + customer_probability: float = Field(..., ge=0, le=1) + + def get_lead_value(self) -> float: + return self.life_time_value * self.customer_probability + + +class Lead(BaseModel): + lead_id: int # could be expended to a UUID later + first_name: str + last_name: str + email_address: EmailStr + phone_number: str + annual_income: AnnualIncome + product_of_interest: ProductOfInterest + lead_value: Optional[LeadValue] diff --git a/src/database/parsers.py b/src/database/parsers.py new file mode 100644 index 0000000..ec0a6bd --- /dev/null +++ b/src/database/parsers.py @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Felix Zailskas + +from typing import Dict + +from database.models import AnnualIncome, Lead, LeadValue, ProductOfInterest + + +class LeadParser: + @staticmethod + def parse_lead_from_dict(data: Dict) -> Lead: + customer_probability = ( + data["customer_probability"] + if "customer_probability" in data.keys() + else None + ) + life_time_value = ( + data["life_time_value"] if "life_time_value" in data.keys() else None + ) + + if customer_probability is not None and life_time_value is not None: + lead_value = LeadValue( + life_time_value=life_time_value, + customer_probability=customer_probability, + ) + else: + lead_value = None + + for income_value in AnnualIncome: + annual_income = income_value + if data["annual_income"] < income_value: + break + + return Lead( + lead_id=data["lead_id"], + first_name=data["first_name"], + last_name=data["last_name"], + email_address=data["email_address"], + phone_number=data["phone_number"], + annual_income=annual_income, + product_of_interest=ProductOfInterest(data["product_of_interest"]), + lead_value=lead_value, + ) diff --git a/src/evp/evp.py b/src/evp/evp.py index 9d43698..16809ad 100644 --- a/src/evp/evp.py +++ b/src/evp/evp.py @@ -5,20 +5,7 @@ from sklearn.linear_model import LinearRegression from database import get_database - - -class LeadValue: - def __init__( - self, lifetime_value: float = 0, customer_probability: float = 0 - ) -> None: - assert ( - 0.0 <= customer_probability <= 1.0 - ), "Probability of becoming a customer must be between 0.0 and 1.0" - self.life_time_value = lifetime_value - self.customer_probability = customer_probability - - def get_lead_value(self) -> float: - return self.life_time_value * self.customer_probability +from database.models import LeadValue class EstimatedValuePredictor: @@ -26,22 +13,23 @@ def __init__(self) -> None: self.probability_predictor = LinearRegression() self.life_time_value_predictor = LinearRegression() - data = get_database().get_all_entries() - X = np.random.random((len(data), len(data))) + all_leads = get_database().get_all_leads() + X = np.random.random((len(all_leads), len(all_leads))) y_probability = np.array( - [item["customer_probability"] for item in data.values()] + [lead.lead_value.customer_probability for lead in all_leads] ) - y_value = np.array([item["customer_probability"] for item in data.values()]) + y_value = np.array([lead.lead_value.life_time_value for lead in all_leads]) self.probability_predictor.fit(X, y_probability) self.life_time_value_predictor.fit(X, y_value) def estimate_value(self, lead_id) -> LeadValue: # make call to data base to retrieve relevant fields for this lead - lead_data = get_database().get_entry_by_id(lead_id) + lead = get_database().get_lead_by_id(lead_id) # preprocess lead_data to get feature vector for our ML model - feature_vector = np.random.random((1, 5)) + feature_vector = np.zeros((1, 5)) + feature_vector[0][lead.lead_id] = 1.0 # use the models to predict required values lead_value_pred = self.life_time_value_predictor.predict(feature_vector) @@ -50,4 +38,10 @@ def estimate_value(self, lead_id) -> LeadValue: 1 + np.exp(-self.probability_predictor.predict(feature_vector)) ) - return LeadValue(lead_value_pred, cust_prob_pred) + lead.lead_value = LeadValue( + life_time_value=lead_value_pred, customer_probability=cust_prob_pred + ) + get_database().update_lead(lead) + + # might not need to return here if the database is updated by this function + return lead.lead_value diff --git a/src/evp_demo.py b/src/evp_demo.py index 5179001..b2fb87b 100644 --- a/src/evp_demo.py +++ b/src/evp_demo.py @@ -4,23 +4,23 @@ from database import get_database from evp.evp import EstimatedValuePredictor -lead_id = 0 +lead_id = 1 -lead_data = get_database().get_entry_by_id(lead_id) +lead = get_database().get_lead_by_id(lead_id) evp = EstimatedValuePredictor() lead_value = evp.estimate_value(lead_id) print( f""" - Dummy prediction for {lead_id=}: + Dummy prediction for lead#{lead.lead_id}: - Data: - {lead_data} + Lead: + {lead} - This lead has a predicted probability of {lead_value.customer_probability} to become a customer. - This lead has a predicted life time value of {lead_value.life_time_value}. + This lead has a predicted probability of {lead_value.customer_probability:.2f} to become a customer. + This lead has a predicted life time value of {lead_value.life_time_value:.2f}. - This results in a total lead value of {lead_value.get_lead_value()}. + This results in a total lead value of {lead_value.get_lead_value():.2f}. """ ) diff --git a/src/test_dummy.py b/tests/test_dummy.py similarity index 100% rename from src/test_dummy.py rename to tests/test_dummy.py From a7ad410b40ffd690769e08b0649a1c304287b129 Mon Sep 17 00:00:00 2001 From: Felix Zailskas Date: Fri, 3 Nov 2023 09:15:18 +0100 Subject: [PATCH 10/19] Replcaed for loop by list comprehension Signed-off-by: Felix Zailskas --- src/database/database_dummy.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/database/database_dummy.py b/src/database/database_dummy.py index b361fac..6635c76 100644 --- a/src/database/database_dummy.py +++ b/src/database/database_dummy.py @@ -18,10 +18,7 @@ def get_lead_by_id(self, id_: int) -> Lead: return LeadParser.parse_lead_from_dict(self.data[id_]) def get_all_leads(self) -> List[Lead]: - leads = [] - for entry in self.data.values(): - leads.append(LeadParser.parse_lead_from_dict(entry)) - return leads + return [LeadParser.parse_lead_from_dict(entry) for entry in self.data.values()] def update_lead(self, lead: Lead): print(f"Updating database entry for lead#{lead.lead_id}") From 4d2f25fb9f98d5b575ad6cda57d70bdef4b4047c Mon Sep 17 00:00:00 2001 From: Felix Zailskas Date: Fri, 3 Nov 2023 09:31:54 +0100 Subject: [PATCH 11/19] Add test for EVP. Set training matrix to identity for equal results on each execution with dummy data. Signed-off-by: Felix Zailskas --- src/evp/evp.py | 2 +- tests/test_evp.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 tests/test_evp.py diff --git a/src/evp/evp.py b/src/evp/evp.py index 16809ad..f43be65 100644 --- a/src/evp/evp.py +++ b/src/evp/evp.py @@ -14,7 +14,7 @@ def __init__(self) -> None: self.life_time_value_predictor = LinearRegression() all_leads = get_database().get_all_leads() - X = np.random.random((len(all_leads), len(all_leads))) + X = np.identity(len(all_leads)) y_probability = np.array( [lead.lead_value.customer_probability for lead in all_leads] ) diff --git a/tests/test_evp.py b/tests/test_evp.py new file mode 100644 index 0000000..dd3127a --- /dev/null +++ b/tests/test_evp.py @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Felix Zailskas + +import os +import sys + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) + +from database import get_database +from database.models import LeadValue +from evp.evp import EstimatedValuePredictor + + +def test_estimate_value(): + leads = get_database().get_all_leads() + evp = EstimatedValuePredictor() + for lead in leads: + value = evp.estimate_value(lead.lead_id) + assert type(value) == LeadValue From cefa71fa01b36c68bd506abe5d276cf329a14f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucca=20Baumg=C3=A4rtner?= <44930425+luccalb@users.noreply.github.com> Date: Fri, 3 Nov 2023 11:33:30 +0100 Subject: [PATCH 12/19] Feature/evp skeleton fix pipeline (#40) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add pipfile.lock, make pipenv install verbose Signed-off-by: Lucca Baumgärtner * enable caching pipenv deps in pipeline Signed-off-by: Lucca Baumgärtner * check if moving the test file fixes it Signed-off-by: Lucca Baumgärtner * add __init__.py to tests folder Signed-off-by: Lucca Baumgärtner * refactor test Signed-off-by: Lucca Baumgärtner * refactor test Signed-off-by: Lucca Baumgärtner * add all dev deps to Pipfile Signed-off-by: Lucca Baumgärtner * add all dev deps to Pipfile Signed-off-by: Lucca Baumgärtner * add all dev deps to Pipfile Signed-off-by: Lucca Baumgärtner * minor changes to pipeline Signed-off-by: Lucca Baumgärtner * minor changes to pipeline Signed-off-by: Lucca Baumgärtner --------- Signed-off-by: Lucca Baumgärtner --- .github/workflows/python-app.yml | 10 +- .gitignore | 2 +- Pipfile | 4 +- Pipfile.lock | 521 +++++++++++++++++++++++++++++++ Pipfile.lock.license | 2 + 5 files changed, 531 insertions(+), 8 deletions(-) create mode 100644 Pipfile.lock create mode 100644 Pipfile.lock.license diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 894bfd9..c91494b 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -46,8 +46,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - name: Set up Python 3.10 uses: actions/setup-python@v4 with: @@ -55,15 +53,15 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest pipenv + pip install pipenv # if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - pipenv install + pipenv install --dev - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + pipenv run flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + pipenv run flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - name: Test with pytest run: | pipenv run pytest diff --git a/.gitignore b/.gitignore index 55621e2..8a82b42 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ pids # Python *.pyc __pycache__/ -Pipfile.lock +# Pipfile.lock # Jupyter Notebook .ipynb_checkpoints diff --git a/Pipfile b/Pipfile index 36e2718..b3c5596 100644 --- a/Pipfile +++ b/Pipfile @@ -7,7 +7,9 @@ verify_ssl = true name = "pypi" [dev-packages] -pytest = "==7.4.3" +pytest = "*" +pre-commit = "*" +flake8 = "*" [packages] numpy = "==1.26.1" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..9cdeccc --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,521 @@ +{ + "_meta": { + "hash": { + "sha256": "78dc89e3c6d794915d2a09753d3dd82dd59b38c1d452db12cfd2b306b5186ced" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "annotated-types": { + "hashes": [ + "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43", + "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d" + ], + "markers": "python_version >= '3.8'", + "version": "==0.6.0" + }, + "dnspython": { + "hashes": [ + "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8", + "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984" + ], + "markers": "python_version >= '3.8' and python_version < '4.0'", + "version": "==2.4.2" + }, + "email-validator": { + "hashes": [ + "sha256:4496ecc949b51e42d1c9e6159d57cd04ef017af57d2e366ed7fd998f1bf8af69", + "sha256:5f511cca8856bb03251d6292ba59e7f98978aae13fa5823ddd8bf885c56a6260" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.1.0" + }, + "idna": { + "hashes": [ + "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4", + "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2" + ], + "markers": "python_version >= '3.5'", + "version": "==3.4" + }, + "joblib": { + "hashes": [ + "sha256:92f865e621e17784e7955080b6d042489e3b8e294949cc44c6eac304f59772b1", + "sha256:ef4331c65f239985f3f2220ecc87db222f08fd22097a3dd5698f693875f8cbb9" + ], + "markers": "python_version >= '3.7'", + "version": "==1.3.2" + }, + "numpy": { + "hashes": [ + "sha256:06934e1a22c54636a059215d6da99e23286424f316fddd979f5071093b648668", + "sha256:1c59c046c31a43310ad0199d6299e59f57a289e22f0f36951ced1c9eac3665b9", + "sha256:1d1bd82d539607951cac963388534da3b7ea0e18b149a53cf883d8f699178c0f", + "sha256:1e11668d6f756ca5ef534b5be8653d16c5352cbb210a5c2a79ff288e937010d5", + "sha256:3649d566e2fc067597125428db15d60eb42a4e0897fc48d28cb75dc2e0454e53", + "sha256:59227c981d43425ca5e5c01094d59eb14e8772ce6975d4b2fc1e106a833d5ae2", + "sha256:6081aed64714a18c72b168a9276095ef9155dd7888b9e74b5987808f0dd0a974", + "sha256:6965888d65d2848e8768824ca8288db0a81263c1efccec881cb35a0d805fcd2f", + "sha256:76ff661a867d9272cd2a99eed002470f46dbe0943a5ffd140f49be84f68ffc42", + "sha256:78ca54b2f9daffa5f323f34cdf21e1d9779a54073f0018a3094ab907938331a2", + "sha256:82e871307a6331b5f09efda3c22e03c095d957f04bf6bc1804f30048d0e5e7af", + "sha256:8ab9163ca8aeb7fd32fe93866490654d2f7dda4e61bc6297bf72ce07fdc02f67", + "sha256:9696aa2e35cc41e398a6d42d147cf326f8f9d81befcb399bc1ed7ffea339b64e", + "sha256:97e5d6a9f0702c2863aaabf19f0d1b6c2628fbe476438ce0b5ce06e83085064c", + "sha256:9f42284ebf91bdf32fafac29d29d4c07e5e9d1af862ea73686581773ef9e73a7", + "sha256:a03fb25610ef560a6201ff06df4f8105292ba56e7cdd196ea350d123fc32e24e", + "sha256:a5b411040beead47a228bde3b2241100454a6abde9df139ed087bd73fc0a4908", + "sha256:af22f3d8e228d84d1c0c44c1fbdeb80f97a15a0abe4f080960393a00db733b66", + "sha256:afd5ced4e5a96dac6725daeb5242a35494243f2239244fad10a90ce58b071d24", + "sha256:b9d45d1dbb9de84894cc50efece5b09939752a2d75aab3a8b0cef6f3a35ecd6b", + "sha256:bb894accfd16b867d8643fc2ba6c8617c78ba2828051e9a69511644ce86ce83e", + "sha256:c8c6c72d4a9f831f328efb1312642a1cafafaa88981d9ab76368d50d07d93cbe", + "sha256:cd7837b2b734ca72959a1caf3309457a318c934abef7a43a14bb984e574bbb9a", + "sha256:cdd9ec98f0063d93baeb01aad472a1a0840dee302842a2746a7a8e92968f9575", + "sha256:d1cfc92db6af1fd37a7bb58e55c8383b4aa1ba23d012bdbba26b4bcca45ac297", + "sha256:d1d2c6b7dd618c41e202c59c1413ef9b2c8e8a15f5039e344af64195459e3104", + "sha256:d2984cb6caaf05294b8466966627e80bf6c7afd273279077679cb010acb0e5ab", + "sha256:d58e8c51a7cf43090d124d5073bc29ab2755822181fcad978b12e144e5e5a4b3", + "sha256:d78f269e0c4fd365fc2992c00353e4530d274ba68f15e968d8bc3c69ce5f5244", + "sha256:dcfaf015b79d1f9f9c9fd0731a907407dc3e45769262d657d754c3a028586124", + "sha256:e44ccb93f30c75dfc0c3aa3ce38f33486a75ec9abadabd4e59f114994a9c4617", + "sha256:e509cbc488c735b43b5ffea175235cec24bbc57b227ef1acc691725beb230d1c" + ], + "index": "pypi", + "markers": "python_version < '3.13' and python_version >= '3.9'", + "version": "==1.26.1" + }, + "pydantic": { + "hashes": [ + "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7", + "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==2.4.2" + }, + "pydantic-core": { + "hashes": [ + "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e", + "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33", + "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7", + "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7", + "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea", + "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4", + "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0", + "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7", + "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94", + "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff", + "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82", + "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd", + "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893", + "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e", + "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d", + "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901", + "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9", + "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c", + "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7", + "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891", + "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f", + "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a", + "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9", + "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5", + "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e", + "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a", + "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c", + "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f", + "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514", + "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b", + "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302", + "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096", + "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0", + "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27", + "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884", + "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a", + "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357", + "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430", + "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221", + "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325", + "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4", + "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05", + "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55", + "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875", + "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970", + "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc", + "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6", + "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f", + "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b", + "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d", + "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15", + "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118", + "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee", + "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e", + "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6", + "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208", + "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede", + "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3", + "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e", + "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada", + "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175", + "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a", + "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c", + "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f", + "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58", + "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f", + "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a", + "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a", + "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921", + "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e", + "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904", + "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776", + "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52", + "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf", + "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8", + "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f", + "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b", + "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63", + "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c", + "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f", + "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468", + "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e", + "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab", + "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2", + "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb", + "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb", + "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132", + "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b", + "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607", + "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934", + "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698", + "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e", + "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561", + "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de", + "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b", + "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a", + "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595", + "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402", + "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881", + "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429", + "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5", + "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7", + "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c", + "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531", + "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6", + "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521" + ], + "markers": "python_version >= '3.7'", + "version": "==2.10.1" + }, + "scikit-learn": { + "hashes": [ + "sha256:0402638c9a7c219ee52c94cbebc8fcb5eb9fe9c773717965c1f4185588ad3107", + "sha256:0ee107923a623b9f517754ea2f69ea3b62fc898a3641766cb7deb2f2ce450161", + "sha256:1215e5e58e9880b554b01187b8c9390bf4dc4692eedeaf542d3273f4785e342c", + "sha256:15e1e94cc23d04d39da797ee34236ce2375ddea158b10bee3c343647d615581d", + "sha256:18424efee518a1cde7b0b53a422cde2f6625197de6af36da0b57ec502f126157", + "sha256:1d08ada33e955c54355d909b9c06a4789a729977f165b8bae6f225ff0a60ec4a", + "sha256:3271552a5eb16f208a6f7f617b8cc6d1f137b52c8a1ef8edf547db0259b2c9fb", + "sha256:35a22e8015048c628ad099da9df5ab3004cdbf81edc75b396fd0cff8699ac58c", + "sha256:535805c2a01ccb40ca4ab7d081d771aea67e535153e35a1fd99418fcedd1648a", + "sha256:5b2de18d86f630d68fe1f87af690d451388bb186480afc719e5f770590c2ef6c", + "sha256:61a6efd384258789aa89415a410dcdb39a50e19d3d8410bd29be365bcdd512d5", + "sha256:64381066f8aa63c2710e6b56edc9f0894cc7bf59bd71b8ce5613a4559b6145e0", + "sha256:67f37d708f042a9b8d59551cf94d30431e01374e00dc2645fa186059c6c5d78b", + "sha256:6c43290337f7a4b969d207e620658372ba3c1ffb611f8bc2b6f031dc5c6d1d03", + "sha256:6fb6bc98f234fda43163ddbe36df8bcde1d13ee176c6dc9b92bb7d3fc842eb66", + "sha256:763f0ae4b79b0ff9cca0bf3716bcc9915bdacff3cebea15ec79652d1cc4fa5c9", + "sha256:785a2213086b7b1abf037aeadbbd6d67159feb3e30263434139c98425e3dcfcf", + "sha256:8db94cd8a2e038b37a80a04df8783e09caac77cbe052146432e67800e430c028", + "sha256:a19f90f95ba93c1a7f7924906d0576a84da7f3b2282ac3bfb7a08a32801add93", + "sha256:a2f54c76accc15a34bfb9066e6c7a56c1e7235dda5762b990792330b52ccfb05", + "sha256:b8692e395a03a60cd927125eef3a8e3424d86dde9b2370d544f0ea35f78a8073", + "sha256:cb06f8dce3f5ddc5dee1715a9b9f19f20d295bed8e3cd4fa51e1d050347de525", + "sha256:dc9002fc200bed597d5d34e90c752b74df516d592db162f756cc52836b38fe0e", + "sha256:e326c0eb5cf4d6ba40f93776a20e9a7a69524c4db0757e7ce24ba222471ee8a1", + "sha256:ed932ea780517b00dae7431e031faae6b49b20eb6950918eb83bd043237950e0", + "sha256:fc4144a5004a676d5022b798d9e573b05139e77f271253a4703eed295bde0433" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==1.3.2" + }, + "scipy": { + "hashes": [ + "sha256:00f325434b6424952fbb636506f0567898dca7b0f7654d48f1c382ea338ce9a3", + "sha256:033c3fd95d55012dd1148b201b72ae854d5086d25e7c316ec9850de4fe776929", + "sha256:0d3a136ae1ff0883fffbb1b05b0b2fea251cb1046a5077d0b435a1839b3e52b7", + "sha256:15f237e890c24aef6891c7d008f9ff7e758c6ef39a2b5df264650eb7900403c0", + "sha256:370f569c57e1d888304052c18e58f4a927338eafdaef78613c685ca2ea0d1fa0", + "sha256:3e1a8a4657673bfae1e05e1e1d6e94b0cabe5ed0c7c144c8aa7b7dbb774ce5c1", + "sha256:4b4bb134c7aa457e26cc6ea482b016fef45db71417d55cc6d8f43d799cdf9ef2", + "sha256:5305792c7110e32ff155aed0df46aa60a60fc6e52cd4ee02cdeb67eaccd5356e", + "sha256:5664e364f90be8219283eeb844323ff8cd79d7acbd64e15eb9c46b9bc7f6a42a", + "sha256:5f290cf561a4b4edfe8d1001ee4be6da60c1c4ea712985b58bf6bc62badee221", + "sha256:74e89dc5e00201e71dd94f5f382ab1c6a9f3ff806c7d24e4e90928bb1aafb280", + "sha256:7abda0e62ef00cde826d441485e2e32fe737bdddee3324e35c0e01dee65e2a88", + "sha256:90271dbde4be191522b3903fc97334e3956d7cfb9cce3f0718d0ab4fd7d8bfd6", + "sha256:91770cb3b1e81ae19463b3c235bf1e0e330767dca9eb4cd73ba3ded6c4151e4d", + "sha256:925c6f09d0053b1c0f90b2d92d03b261e889b20d1c9b08a3a51f61afc5f58165", + "sha256:9885e3e4f13b2bd44aaf2a1a6390a11add9f48d5295f7a592393ceb8991577a3", + "sha256:9ea7f579182d83d00fed0e5c11a4aa5ffe01460444219dedc448a36adf0c3917", + "sha256:a63d1ec9cadecce838467ce0631c17c15c7197ae61e49429434ba01d618caa83", + "sha256:bae66a2d7d5768eaa33008fa5a974389f167183c87bf39160d3fefe6664f8ddc", + "sha256:bba4d955f54edd61899776bad459bf7326e14b9fa1c552181f0479cc60a568cd", + "sha256:c77da50c9a91e23beb63c2a711ef9e9ca9a2060442757dffee34ea41847d8156", + "sha256:d2f6dee6cbb0e263b8142ed587bc93e3ed5e777f1f75448d24fb923d9fd4dce6", + "sha256:dfcc1552add7cb7c13fb70efcb2389d0624d571aaf2c80b04117e2755a0c5d15", + "sha256:e04aa19acc324a1a076abb4035dabe9b64badb19f76ad9c798bde39d41025cdc", + "sha256:e1f97cd89c0fe1a0685f8f89d85fa305deb3067d0668151571ba50913e445820" + ], + "markers": "python_version < '3.13' and python_version >= '3.9'", + "version": "==1.11.3" + }, + "threadpoolctl": { + "hashes": [ + "sha256:2b7818516e423bdaebb97c723f86a7c6b0a83d3f3b0970328d66f4d9104dc032", + "sha256:c96a0ba3bdddeaca37dc4cc7344aafad41cdb8c313f74fdfe387a867bba93355" + ], + "markers": "python_version >= '3.8'", + "version": "==3.2.0" + }, + "typing-extensions": { + "hashes": [ + "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0", + "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef" + ], + "markers": "python_version >= '3.8'", + "version": "==4.8.0" + } + }, + "develop": { + "cfgv": { + "hashes": [ + "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", + "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560" + ], + "markers": "python_version >= '3.8'", + "version": "==3.4.0" + }, + "colorama": { + "hashes": [ + "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", + "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.4.6" + }, + "distlib": { + "hashes": [ + "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057", + "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8" + ], + "version": "==0.3.7" + }, + "exceptiongroup": { + "hashes": [ + "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9", + "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3" + ], + "markers": "python_version < '3.11'", + "version": "==1.1.3" + }, + "filelock": { + "hashes": [ + "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e", + "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c" + ], + "markers": "python_version >= '3.8'", + "version": "==3.13.1" + }, + "flake8": { + "hashes": [ + "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23", + "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5" + ], + "index": "pypi", + "markers": "python_full_version >= '3.8.1'", + "version": "==6.1.0" + }, + "identify": { + "hashes": [ + "sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75", + "sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d" + ], + "markers": "python_version >= '3.8'", + "version": "==2.5.31" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" + }, + "mccabe": { + "hashes": [ + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" + ], + "markers": "python_version >= '3.6'", + "version": "==0.7.0" + }, + "nodeenv": { + "hashes": [ + "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2", + "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", + "version": "==1.8.0" + }, + "packaging": { + "hashes": [ + "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", + "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + ], + "markers": "python_version >= '3.7'", + "version": "==23.2" + }, + "platformdirs": { + "hashes": [ + "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3", + "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e" + ], + "markers": "python_version >= '3.7'", + "version": "==3.11.0" + }, + "pluggy": { + "hashes": [ + "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12", + "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7" + ], + "markers": "python_version >= '3.8'", + "version": "==1.3.0" + }, + "pre-commit": { + "hashes": [ + "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32", + "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==3.5.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", + "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67" + ], + "markers": "python_version >= '3.8'", + "version": "==2.11.1" + }, + "pyflakes": { + "hashes": [ + "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", + "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc" + ], + "markers": "python_version >= '3.8'", + "version": "==3.1.0" + }, + "pytest": { + "hashes": [ + "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac", + "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==7.4.3" + }, + "pyyaml": { + "hashes": [ + "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", + "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", + "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", + "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", + "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", + "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", + "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", + "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", + "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", + "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", + "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", + "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", + "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", + "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", + "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", + "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", + "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", + "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", + "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", + "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", + "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", + "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", + "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", + "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", + "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", + "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", + "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", + "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", + "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", + "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", + "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", + "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", + "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", + "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", + "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", + "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", + "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", + "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", + "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", + "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", + "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", + "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", + "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", + "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", + "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", + "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", + "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", + "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", + "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", + "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + ], + "markers": "python_version >= '3.6'", + "version": "==6.0.1" + }, + "setuptools": { + "hashes": [ + "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87", + "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a" + ], + "markers": "python_version >= '3.8'", + "version": "==68.2.2" + }, + "tomli": { + "hashes": [ + "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" + ], + "markers": "python_version < '3.11'", + "version": "==2.0.1" + }, + "virtualenv": { + "hashes": [ + "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af", + "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381" + ], + "markers": "python_version >= '3.7'", + "version": "==20.24.6" + } + } +} diff --git a/Pipfile.lock.license b/Pipfile.lock.license new file mode 100644 index 0000000..e64ca02 --- /dev/null +++ b/Pipfile.lock.license @@ -0,0 +1,2 @@ +SPDX-License-Identifier: CC-BY-4.0 +SPDX-FileCopyrightText: 2023 Lucca Baumgärtner From 5d6e3e2287a05162601d15e0247dbdfea5f7a481 Mon Sep 17 00:00:00 2001 From: Felix Zailskas Date: Fri, 3 Nov 2023 16:09:33 +0100 Subject: [PATCH 13/19] Added tests for the LeadParser Signed-off-by: Felix Zailskas --- src/database/models.py | 2 +- src/database/parsers.py | 3 +- tests/conftest.py | 24 +++++ tests/test_leadparser.py | 187 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 tests/conftest.py create mode 100644 tests/test_leadparser.py diff --git a/src/database/models.py b/src/database/models.py index 2cc4a0a..4932c89 100644 --- a/src/database/models.py +++ b/src/database/models.py @@ -31,7 +31,7 @@ class ProductOfInterest(str, Enum): class LeadValue(BaseModel): - life_time_value: float + life_time_value: float = Field(..., ge=0) customer_probability: float = Field(..., ge=0, le=1) def get_lead_value(self) -> float: diff --git a/src/database/parsers.py b/src/database/parsers.py index ec0a6bd..5d73a7c 100644 --- a/src/database/parsers.py +++ b/src/database/parsers.py @@ -26,10 +26,11 @@ def parse_lead_from_dict(data: Dict) -> Lead: else: lead_value = None + annual_income = AnnualIncome.Nothing for income_value in AnnualIncome: - annual_income = income_value if data["annual_income"] < income_value: break + annual_income = income_value return Lead( lead_id=data["lead_id"], diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c0563d1 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Felix Zailskas + +import json +from typing import Dict + +import pytest + + +@pytest.fixture +def create_lead_dict(request) -> Dict: + lead_value_adjustments = request.param + lead_data = { + "lead_id": 0, + "annual_income": 0, + "product_of_interest": "Nothing", + "first_name": "Manu", + "last_name": "Musterperson", + "phone_number": "49123123123", + "email_address": "test@test.de", + } + for key, value in lead_value_adjustments.items(): + lead_data[key] = value + yield lead_data diff --git a/tests/test_leadparser.py b/tests/test_leadparser.py new file mode 100644 index 0000000..7069440 --- /dev/null +++ b/tests/test_leadparser.py @@ -0,0 +1,187 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Felix Zailskas + +import os +import sys + +import pytest +from pydantic import ValidationError + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) + +from database.models import AnnualIncome, LeadValue, ProductOfInterest +from database.parsers import LeadParser + + +@pytest.mark.parametrize( + "create_lead_dict, result", + [ + ({"lead_id": 0}, 0), + ({"lead_id": 12999}, 12999), + ({"lead_id": 40000}, 40000), + ({"lead_id": 42}, 42), + ], + indirect=["create_lead_dict"], +) +def test_lead_id(create_lead_dict, result): + lead = LeadParser.parse_lead_from_dict(create_lead_dict) + assert lead.lead_id == result + + +@pytest.mark.parametrize( + "create_lead_dict, result", + [ + ({"first_name": ""}, ""), + ({"first_name": "Felix"}, "Felix"), + ({"first_name": '|%$&"\n'}, '|%$&"\n'), + ({"first_name": "42"}, "42"), + ], + indirect=["create_lead_dict"], +) +def test_first_name(create_lead_dict, result): + lead = LeadParser.parse_lead_from_dict(create_lead_dict) + assert lead.first_name == result + + +@pytest.mark.parametrize( + "create_lead_dict, result", + [ + ({"last_name": ""}, ""), + ({"last_name": "Felix"}, "Felix"), + ({"last_name": '|%$&"\n'}, '|%$&"\n'), + ({"last_name": "42"}, "42"), + ], + indirect=["create_lead_dict"], +) +def test_last_name(create_lead_dict, result): + lead = LeadParser.parse_lead_from_dict(create_lead_dict) + assert lead.last_name == result + + +@pytest.mark.parametrize( + "create_lead_dict, result, expected_exception", + [ + ({"email_address": "thisisanemail"}, "", ValidationError), + ({"email_address": "email@domain.com"}, "email@domain.com", None), + ({"email_address": "first.last@google.de"}, "first.last@google.de", None), + ({"email_address": ""}, "", ValidationError), + ({"email_address": "first.last@domain"}, "", ValidationError), + ({"email_address": "first.last.com"}, "", ValidationError), + ], + indirect=["create_lead_dict"], +) +def test_email_address(create_lead_dict, result, expected_exception): + if expected_exception: + with pytest.raises(expected_exception): + lead = LeadParser.parse_lead_from_dict(create_lead_dict) + else: + lead = LeadParser.parse_lead_from_dict(create_lead_dict) + assert lead.email_address == result + + +@pytest.mark.parametrize( + "create_lead_dict, result", + [ + ({"phone_number": "49123123123"}, "49123123123"), + ({"phone_number": "31321321321"}, "31321321321"), + ({"phone_number": ""}, ""), + ], + indirect=["create_lead_dict"], +) +def test_phone_number(create_lead_dict, result): + lead = LeadParser.parse_lead_from_dict(create_lead_dict) + assert lead.phone_number == result + + +@pytest.mark.parametrize( + "create_lead_dict, result", + [ + ({"annual_income": 0}, AnnualIncome.Nothing), + ({"annual_income": 12999}, AnnualIncome.Class1), + ({"annual_income": 40000}, AnnualIncome.Class2), + ({"annual_income": 75029}, AnnualIncome.Class3), + ({"annual_income": 144586}, AnnualIncome.Class4), + ({"annual_income": 200001}, AnnualIncome.Class5), + ({"annual_income": 599999}, AnnualIncome.Class6), + ({"annual_income": 1000000}, AnnualIncome.Class7), + ({"annual_income": 1309481}, AnnualIncome.Class8), + ({"annual_income": 4921093}, AnnualIncome.Class9), + ({"annual_income": 10000000}, AnnualIncome.Class10), + ], + indirect=["create_lead_dict"], +) +def test_annual_income(create_lead_dict, result): + lead = LeadParser.parse_lead_from_dict(create_lead_dict) + assert lead.annual_income == result + + +@pytest.mark.parametrize( + "create_lead_dict, result, expected_exception", + [ + ({"product_of_interest": ""}, "", ValueError), + ({"product_of_interest": "Nothing"}, ProductOfInterest.Nothing, None), + ({"product_of_interest": "Terminals"}, ProductOfInterest.Terminals, None), + ( + {"product_of_interest": "Cash Register System"}, + ProductOfInterest.CashRegisterSystem, + None, + ), + ( + {"product_of_interest": "Business Account"}, + ProductOfInterest.BusinessAccount, + None, + ), + ({"product_of_interest": "All"}, ProductOfInterest.All, None), + ({"product_of_interest": "Other"}, ProductOfInterest.Other, None), + ({"product_of_interest": "Bisuness Account"}, "", ValueError), + ], + indirect=["create_lead_dict"], +) +def test_product_of_interest(create_lead_dict, result, expected_exception): + if expected_exception: + with pytest.raises(expected_exception): + lead = LeadParser.parse_lead_from_dict(create_lead_dict) + else: + lead = LeadParser.parse_lead_from_dict(create_lead_dict) + assert lead.product_of_interest == result + + +@pytest.mark.parametrize( + "create_lead_dict, result, expected_exception", + [ + ({}, None, None), + ({"customer_probability": 0.0}, None, None), + ({"life_time_value": 20000}, None, None), + ( + {"customer_probability": 0.0, "life_time_value": 20000}, + LeadValue(customer_probability=0.0, life_time_value=20000), + None, + ), + ( + {"customer_probability": 1.0, "life_time_value": 0}, + LeadValue(customer_probability=1.0, life_time_value=0), + None, + ), + ({"customer_probability": 0.5, "life_time_value": -1}, None, ValidationError), + ({"customer_probability": -0.1, "life_time_value": 42}, None, ValidationError), + ({"customer_probability": 1.1, "life_time_value": 42}, None, ValidationError), + ( + {"customer_probability": 0.5, "life_time_value": 0}, + LeadValue(customer_probability=0.5, life_time_value=0), + None, + ), + ( + {"customer_probability": 0.5, "life_time_value": 300}, + LeadValue(customer_probability=0.5, life_time_value=300), + None, + ), + ], + indirect=["create_lead_dict"], +) +def test_lead_value(create_lead_dict, result, expected_exception): + if expected_exception: + with pytest.raises(expected_exception): + lead = LeadParser.parse_lead_from_dict(create_lead_dict) + else: + lead = LeadParser.parse_lead_from_dict(create_lead_dict) + assert lead.lead_value == result From 08086ca5fa68d208f2497478c4df1879a794475f Mon Sep 17 00:00:00 2001 From: Berkay Bozkurt Date: Sun, 5 Nov 2023 17:09:54 +0100 Subject: [PATCH 14/19] controller functionality implemented. #36 Signed-off-by: Berkay Bozkurt --- src/controller/Controller.py | 195 +++++++++++++++++++++++++++++++++-- src/controller/messenger.py | 39 +++++++ 2 files changed, 225 insertions(+), 9 deletions(-) create mode 100644 src/controller/messenger.py diff --git a/src/controller/Controller.py b/src/controller/Controller.py index 980aab0..adb0118 100644 --- a/src/controller/Controller.py +++ b/src/controller/Controller.py @@ -1,9 +1,13 @@ # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: 2023 Berkay Bozkurt -from threading import Lock +from queue import Queue +from threading import Lock, Thread, current_thread +from time import sleep from typing import Any +from messenger import Message, MessageType, create_data_message + class ControllerMeta(type): @@ -11,18 +15,191 @@ class ControllerMeta(type): Thread safe singleton implementation of Controller """ - _instances = {} - _lock: Lock = Lock() + _instances = {} # Dictionary to store instances of Controller + _lock: Lock = Lock() # A lock to ensure thread-safety when creating instances - def __call__(self, *args: Any, **kwds: Any): - with self._lock: - if self not in self._instances: - instance = super().__call__(*args, **kwds) - self._instances[self] = instance + def __call__(cls, *args: Any, **kwds: Any): + with cls._lock: + if cls not in cls._instances: + instance = super().__call__( + *args, **kwds + ) # Create a new instance of Controller + cls._instances[ + cls + ] = instance # Store the instance in the _instances dictionary - return self._instances[self] + return cls._instances[cls] # Return the instance of Controller class Controller(metaclass=ControllerMeta): + """ + Controller class with message processing and sending functionality. + """ + def __init__(self, name: str) -> None: self.name = name + self._finish_flag = False + self._finish_flag_lock = Lock() + self._message_queue: Queue[Message] = Queue(0) # Queue for processing messages + self._routing_queue: Queue[Message] = Queue(0) # Queue for routing messages + self._message_queue_processor_thread: Thread = ( + None # Thread for processing messages + ) + self._routing_queue_processor_thread: Thread = ( + None # Thread for routing messages + ) + self._start_message_queue_processing_thread() # Start the message processing thread + self._start_routing_queue_processing_thread() # Start the routing thread + + def _message_queue_processor(self): + while True: + # do read operation with lock to get latest value + with self._finish_flag_lock: + # if set True then exit loop + if self._finish_flag: + break + if not self._message_queue.empty(): + try: + # Get a message from the message queue and process it‚ + msg = self._message_queue.get() + # Simulate processing of the message + print(f"Processing on {msg}") + self._enqueue_routing(msg) + # Simulate completion of processing + print(f"Processed {msg}") + # Handle any errors during message processing + except Exception as e: + print(f"Error while processing message: {e}") + finally: + # Mark the task as done in the processing queue + self._message_queue.task_done() + print(f"Message queue processor thread exited.") + + def _routing_queue_processor(self): + while True: + with self._finish_flag_lock: + if self._finish_flag: + break + if not self._routing_queue.empty(): + try: + # Mark the task as done in the processing queue + msg = self._routing_queue.get() + print(f"Routing {msg}") + if msg.data_type == MessageType.DATA: + self._route_to_EVP(msg) + elif msg.data_type == MessageType.PREDICTION: + self._route_to_BDC(msg) + else: + print(f"Unknown message type: {msg.data_type}") + print(f"Routed {msg}") + # Handle any errors during message routing + except Exception as e: + print(f"Error while routing message: {e}") + finally: + # Mark the task as done in the processing queue + self._routing_queue.task_done() + print(f"Routing queue processor thread exited.") + + # Start the message processing thread + def _start_message_queue_processing_thread(self): + if ( + not self._message_queue_processor_thread + or not self._message_queue_processor_thread.is_alive() + ): + self._message_queue_processor_thread = Thread( + target=self._message_queue_processor, daemon=True + ) + self._message_queue_processor_thread.start() + + # Start the message routing thread + def _start_routing_queue_processing_thread(self): + if ( + not self._routing_queue_processor_thread + or not self._routing_queue_processor_thread.is_alive() + ): + self._routing_queue_processor_thread = Thread( + target=self._routing_queue_processor, daemon=True + ) + self._routing_queue_processor_thread.start() + + def _route_to_BDC(self, msg: Message): + # TODO call the method of base data collector + return + + def _route_to_EVP(self, msg: Message): + # TODO call the method of estimated value predictor + return + + # Enqueue a message in the processing queue + def _enqueue_message(self, msg: Message): + self._message_queue.put(msg) + + # Enqueue a message in the routing queue + def _enqueue_routing(self, msg: Message): + self._routing_queue.put(msg) + + # Public interface to send a message + def send_message(self, msg: Message): + """ + processes message, forwards to related components. + """ + if not self._finish_flag: + self._enqueue_message(msg) + else: + print(f"Controller finished can not send messages... ") + + def finish(self): + """ + finishes controller, after all waiting messages are processed and routed + """ + + # wait till queues are empty. + while not self._message_queue.empty() or not self._routing_queue.empty(): + print(f"Waiting for message and routing threads to finish their jobs... ") + + with self._finish_flag_lock: + # Set the finish flag to signal threads to stop + self._finish_flag = True + + print(f"Finishing threads... ") + + # Wait for the message queue processing thread to finish + if ( + self._message_queue_processor_thread + and self._message_queue_processor_thread.is_alive() + ): + print(f"Finishing message queue processor thread...") + self._message_queue_processor_thread.join() + # Wait for the routing queue processing thread to finish + if ( + self._routing_queue_processor_thread + and self._routing_queue_processor_thread.is_alive() + ): + print(f"Finishing routing queue processor thread...") + self._routing_queue_processor_thread.join() + + # check if there are any elements in queues, if not, all cool! + print(f"Threads finished... ") + print(f"routing queue size... {self._routing_queue.unfinished_tasks}") + print(f"message queue size... {self._message_queue.unfinished_tasks}") + + +if __name__ == "__main__": + c1 = Controller("First Controller") + c2 = Controller("Second Controller") + + if id(c1) == id(c2): + print( + f"Singleton works, both variables contain the same instance. c1 {c1.name} and c2 {c2.name}" + ) + else: + print("Singleton failed, variables contain different instances.") + + msg = create_data_message(2023, {"name": "AMOS"}) + + for item in range(5): + c1.send_message(msg) + + c1.finish() + c1.send_message(msg) + print("All work completed") diff --git a/src/controller/messenger.py b/src/controller/messenger.py new file mode 100644 index 0000000..e832e7e --- /dev/null +++ b/src/controller/messenger.py @@ -0,0 +1,39 @@ +# SPDX-License-Identifier: MIT +# SPDX-FileCopyrightText: 2023 Berkay Bozkurt + +from enum import Enum + + +class MessageType(Enum): + DATA = "data" + PREDICTION = "prediction" + + +class Message: + def __init__(self, sender_name, data_type, data=None, result=None): + self.sender_name = sender_name + self.data_type = data_type + self.data = data if data is not None else {} + self.result = result if result is not None else {} + + +def create_data_message(lead_id, features: {}): + """ + Creates a data message, called by BDC. + """ + message = Message( + "BDC", MessageType.DATA, {"lead_id": lead_id, "features": features} + ) + return message + + +def create_prediction_message(lead_id, prediction_value): + """ + Create a prediction message, called by EVP. + """ + message = Message( + "EVP", + MessageType.PREDICTION, + result={"lead_id": lead_id, "prediction value": prediction_value}, + ) + return message From 7bdb43163d9c181a821d9b9fcf9d29346141638b Mon Sep 17 00:00:00 2001 From: Berkay Bozkurt Date: Mon, 6 Nov 2023 09:30:31 +0100 Subject: [PATCH 15/19] Unused imports are removed #36 Signed-off-by: Berkay Bozkurt --- src/controller/Controller.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/controller/Controller.py b/src/controller/Controller.py index adb0118..287dee6 100644 --- a/src/controller/Controller.py +++ b/src/controller/Controller.py @@ -2,8 +2,7 @@ # SPDX-FileCopyrightText: 2023 Berkay Bozkurt from queue import Queue -from threading import Lock, Thread, current_thread -from time import sleep +from threading import Lock, Thread from typing import Any from messenger import Message, MessageType, create_data_message From 6c55336718c25c47c28769576952dd359c27ea2f Mon Sep 17 00:00:00 2001 From: Berkay Bozkurt Date: Mon, 6 Nov 2023 09:39:57 +0100 Subject: [PATCH 16/19] Message class dict is set to double asterisk #36 Signed-off-by: Berkay Bozkurt --- src/controller/messenger.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/controller/messenger.py b/src/controller/messenger.py index e832e7e..743a22a 100644 --- a/src/controller/messenger.py +++ b/src/controller/messenger.py @@ -17,13 +17,11 @@ def __init__(self, sender_name, data_type, data=None, result=None): self.result = result if result is not None else {} -def create_data_message(lead_id, features: {}): +def create_data_message(lead_id, **features): """ Creates a data message, called by BDC. """ - message = Message( - "BDC", MessageType.DATA, {"lead_id": lead_id, "features": features} - ) + message = Message("BDC", MessageType.DATA, {"lead_id": lead_id, **features}) return message From 8d878a37ccb44ca647262b4a8da1e374d5c9036c Mon Sep 17 00:00:00 2001 From: Berkay Bozkurt Date: Mon, 6 Nov 2023 09:43:57 +0100 Subject: [PATCH 17/19] main deleted from controller.py #36 Signed-off-by: Berkay Bozkurt --- src/controller/Controller.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/controller/Controller.py b/src/controller/Controller.py index 287dee6..6d7d1f8 100644 --- a/src/controller/Controller.py +++ b/src/controller/Controller.py @@ -181,24 +181,3 @@ def finish(self): print(f"Threads finished... ") print(f"routing queue size... {self._routing_queue.unfinished_tasks}") print(f"message queue size... {self._message_queue.unfinished_tasks}") - - -if __name__ == "__main__": - c1 = Controller("First Controller") - c2 = Controller("Second Controller") - - if id(c1) == id(c2): - print( - f"Singleton works, both variables contain the same instance. c1 {c1.name} and c2 {c2.name}" - ) - else: - print("Singleton failed, variables contain different instances.") - - msg = create_data_message(2023, {"name": "AMOS"}) - - for item in range(5): - c1.send_message(msg) - - c1.finish() - c1.send_message(msg) - print("All work completed") From 50bb3c17ed2756106f923c6a11ff903ddcdc2fc4 Mon Sep 17 00:00:00 2001 From: Berkay Bozkurt Date: Mon, 6 Nov 2023 09:49:25 +0100 Subject: [PATCH 18/19] message class extended from BaseModel Signed-off-by: Berkay Bozkurt --- src/controller/messenger.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/controller/messenger.py b/src/controller/messenger.py index 743a22a..f305a2a 100644 --- a/src/controller/messenger.py +++ b/src/controller/messenger.py @@ -2,6 +2,9 @@ # SPDX-FileCopyrightText: 2023 Berkay Bozkurt from enum import Enum +from typing import Dict, Optional + +from pydantic import BaseModel class MessageType(Enum): @@ -9,12 +12,11 @@ class MessageType(Enum): PREDICTION = "prediction" -class Message: - def __init__(self, sender_name, data_type, data=None, result=None): - self.sender_name = sender_name - self.data_type = data_type - self.data = data if data is not None else {} - self.result = result if result is not None else {} +class Message(BaseModel): + sender_name: str + data_type: str + data: Optional[Dict] = {} + result: Optional[Dict] = {} def create_data_message(lead_id, **features): From b9b3f4e64bad396766bd519cdb46c15260ce0fb3 Mon Sep 17 00:00:00 2001 From: Berkay Bozkurt Date: Mon, 6 Nov 2023 11:17:33 +0100 Subject: [PATCH 19/19] small changes in messenger #36 Signed-off-by: Berkay Bozkurt --- src/controller/messenger.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/controller/messenger.py b/src/controller/messenger.py index f305a2a..7af5723 100644 --- a/src/controller/messenger.py +++ b/src/controller/messenger.py @@ -12,9 +12,14 @@ class MessageType(Enum): PREDICTION = "prediction" +class SenderType(Enum): + BDC = "base_data_collector" + EVP = "estimated_value_predictor" + + class Message(BaseModel): - sender_name: str - data_type: str + sender_name: SenderType + data_type: MessageType data: Optional[Dict] = {} result: Optional[Dict] = {} @@ -23,7 +28,11 @@ def create_data_message(lead_id, **features): """ Creates a data message, called by BDC. """ - message = Message("BDC", MessageType.DATA, {"lead_id": lead_id, **features}) + message = Message( + sender_name=SenderType.BDC, + data_type=MessageType.DATA, + data={"lead_id": lead_id, **features}, + ) return message @@ -32,8 +41,8 @@ def create_prediction_message(lead_id, prediction_value): Create a prediction message, called by EVP. """ message = Message( - "EVP", - MessageType.PREDICTION, + sender_name=SenderType.EVP, + data_type=MessageType.PREDICTION, result={"lead_id": lead_id, "prediction value": prediction_value}, ) return message