contactlookup 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,15 @@
1
+ # Initialize logger
2
+ import logging.config
3
+ from pathlib import Path
4
+
5
+ from contactlookup.definitions import LOGGING_CONFIG_PATH
6
+
7
+ # homepath = $HOME
8
+ HOMEPATH = str(Path.home())
9
+ logging.config.fileConfig(
10
+ fname=LOGGING_CONFIG_PATH,
11
+ disable_existing_loggers=False,
12
+ defaults={"log_dir": HOMEPATH},
13
+ )
14
+ logger = logging.getLogger(__name__)
15
+ logger.info("Logger initialized")
@@ -0,0 +1,141 @@
1
+ """Main module for the contactlookup package."""
2
+
3
+ import logging
4
+ import site
5
+ import traceback
6
+ from pathlib import Path
7
+
8
+ import fire
9
+ import uvicorn
10
+
11
+ import contactlookup.controller as app_controller
12
+ from contactlookup.definitions import (
13
+ FILE_DATA_STORE_SERVICE,
14
+ ROOT_DIR,
15
+ SAMPLE_CONTACTS_DIR,
16
+ SAMPLE_CONTACTS_FILE,
17
+ )
18
+ from contactlookup.services.data_store_service import DataStoreService
19
+ from contactlookup.services.file_data_store_service import FileDataStoreService
20
+
21
+
22
+ def _load_contacts_file(service: FileDataStoreService, contacts_file_path: str | None):
23
+ # Ensure the contacts file path is provided.
24
+ # If no file is provided by the user, use the default contacts file:
25
+ # SAMPLE_CONTACTS_DIR/SAMPLE_CONTACTS_FILE in the tests directory.
26
+ logger = logging.getLogger(__name__)
27
+ if not contacts_file_path:
28
+ print("No contacts file provided.")
29
+ logger.info("No contacts file provided. Using default contacts file.")
30
+ # The data directory is in the tests directory which is installed in
31
+ # the site-packages directory.
32
+
33
+ try:
34
+ # First check for tests/data/contacts.vcf in root's directory.
35
+ # If we are developing the package, the contacts file will be in
36
+ # ROOT_DIR/../tests/data/contacts.vcf
37
+ development_contacts_file_path = (
38
+ ROOT_DIR.parent / SAMPLE_CONTACTS_DIR / SAMPLE_CONTACTS_FILE
39
+ )
40
+
41
+ if development_contacts_file_path.exists():
42
+ contacts_file_path = str(development_contacts_file_path)
43
+ logger.info("Application is in development mode.")
44
+ else:
45
+ # We are not developing the package. The contacts file will be
46
+ # in the site-packages directory.
47
+ # Check for tests/data/contacts.vcf in the site-packages directory
48
+ site_packages_dir = Path(site.getsitepackages()[0])
49
+
50
+ contacts_file_path = (
51
+ f"{site_packages_dir}/{SAMPLE_CONTACTS_DIR}/{SAMPLE_CONTACTS_FILE}"
52
+ )
53
+ logger.info("Application is in production mode.")
54
+ print(f"Using default contacts file: {contacts_file_path}")
55
+ except ImportError as e:
56
+ print(
57
+ "Could not find site-packages directory. Please provide a contacts file.",
58
+ )
59
+ stack_trace = traceback.format_exc()
60
+ logger.error(
61
+ "_load_contacts_file|Error loading contacts file: %s. Exiting",
62
+ e,
63
+ )
64
+ logger.error("_load_contacts_file|Stack trace: %s", stack_trace)
65
+ return
66
+
67
+ contacts_file_path = contacts_file_path.strip()
68
+ contacts_file = Path(contacts_file_path)
69
+
70
+ service.set_contacts_file_path(contacts_file)
71
+
72
+
73
+ def _setup(
74
+ data_store_service: str | None = None,
75
+ contacts_file_path: str | None = None,
76
+ ) -> DataStoreService | None:
77
+ """
78
+ data_store_service is used to indicate the type of data store service to
79
+ use.
80
+ Options:
81
+ 1. f: indicates FileDataStoreService
82
+ 2. d: indicates DatabaseDataStoreService (not implemented)
83
+
84
+ If no data_store_service is provided, the default is FileDataStoreService.
85
+ If an invalid data_store_service is provided, the default is FileDataStoreService.
86
+ """
87
+ logger = logging.getLogger(__name__)
88
+ if not data_store_service:
89
+ data_store_service = FILE_DATA_STORE_SERVICE
90
+ data_store_service = data_store_service.strip().lower()
91
+
92
+ logger.info("Using data store service option: %s", data_store_service)
93
+ # Tell the user what data store service is being used
94
+ if data_store_service == FILE_DATA_STORE_SERVICE:
95
+ print("Using FileDataStoreService")
96
+ else:
97
+ # Not implemented databaseconnection. Would require a database connection.
98
+ print("Invalid data store service. Using FileDataStoreService")
99
+ data_store_service = FILE_DATA_STORE_SERVICE
100
+
101
+ service = FileDataStoreService()
102
+ _load_contacts_file(service=service, contacts_file_path=contacts_file_path)
103
+ initialized = service.initialize()
104
+ if not initialized:
105
+ logger.error("Data store service failed to initialize")
106
+ print("Data store service failed to initialize")
107
+ return None
108
+ return service
109
+
110
+
111
+ def cli():
112
+ """Command line interface."""
113
+ fire.Fire(main)
114
+
115
+
116
+ def main(service: str | None = FILE_DATA_STORE_SERVICE, file: str | None = None):
117
+ """Expose API to query contacts.
118
+
119
+ Args:
120
+ service (str | None, optional): The data store service to use. Defaults to FILE_DATA_STORE_SERVICE.
121
+ file (str | None, optional): The path to the contacts file. Defaults to None.
122
+ """
123
+ data_store_service = _setup(data_store_service=service, contacts_file_path=file)
124
+ if not data_store_service:
125
+ return
126
+
127
+ # At this point, the data store service is initialized
128
+ # We can now run the FastAPI application
129
+ logger = logging.getLogger(__name__)
130
+ logger.info("Data store service initialized")
131
+ print("Data store service initialized")
132
+
133
+ app_controller.set_data_store_service(data_store_service)
134
+
135
+ logger.info("Running FastAPI application")
136
+ # Run the FastAPI application
137
+ uvicorn.run(app_controller.app)
138
+
139
+
140
+ if __name__ == "__main__":
141
+ fire.Fire(main)
@@ -0,0 +1,92 @@
1
+ """Controller for the contactlookup app."""
2
+
3
+ from fastapi import FastAPI
4
+
5
+ from contactlookup.services.data_store_service import DataStoreService
6
+
7
+ app = FastAPI()
8
+ service: DataStoreService | None = None
9
+
10
+
11
+ def set_data_store_service(data_store_service: DataStoreService):
12
+ """Set the data store service.
13
+
14
+ In the future, we will implement a database service to store contacts. This
15
+ function will be used to set the database service to be used by the
16
+ controller.
17
+
18
+ Args:
19
+ data_store_service (DataStoreService): The data store service.
20
+ """
21
+ global service
22
+ service = data_store_service
23
+
24
+
25
+ @app.get("/")
26
+ def read_root():
27
+ """Welcome message."""
28
+ msg = "Welcome to the Contact Lookup App. Use the /docs endpoint to see the API documentation."
29
+ return {"message": msg}
30
+
31
+
32
+ @app.get("/contacts")
33
+ def read_contacts():
34
+ """Get all contacts."""
35
+ if not service:
36
+ return {"Error": "Data store service not set"}
37
+ contacts = service.get_contacts()
38
+ return {"contacts": contacts}
39
+
40
+
41
+ @app.get("/contacts/{contact_id}")
42
+ def read_contact(contact_id: int):
43
+ """Get contact by ID."""
44
+ if not service:
45
+ return {"Error": "Data store service not set"}
46
+ contact = service.get_contact(contact_id)
47
+ return {"contact": contact}
48
+
49
+
50
+ @app.get("/contacts/fname/{fname}")
51
+ def read_contacts_by_fname(fname: str):
52
+ """Get contacts by first name."""
53
+ if not service:
54
+ return {"Error": "Data store service not set"}
55
+ contacts = service.get_contacts_by_fname(fname)
56
+ return {"contacts": contacts}
57
+
58
+
59
+ @app.get("/contacts/phone/{phone_number}")
60
+ def read_contacts_by_phone_number(phone_number: str):
61
+ """Get contacts by phone number."""
62
+ if not service:
63
+ return {"Error": "Data store service not set"}
64
+ contacts = service.get_contacts_by_phone_number(phone_number)
65
+ return {"contacts": contacts}
66
+
67
+
68
+ @app.get("/contacts/email/{email}")
69
+ def read_contacts_by_email(email: str):
70
+ """Get contacts by email."""
71
+ if not service:
72
+ return {"Error": "Data store service not set"}
73
+ contacts = service.get_contacts_by_email(email)
74
+ return {"contacts": contacts}
75
+
76
+
77
+ @app.get("/contacts/country/{country}")
78
+ def read_contacts_by_country(country: str):
79
+ """Get contacts by country."""
80
+ if not service:
81
+ return {"Error": "Data store service not set"}
82
+ contacts = service.get_contacts_by_country(country)
83
+ return {"contacts": contacts}
84
+
85
+
86
+ @app.get("/contacts/state/{state}")
87
+ def read_contacts_by_state(state: str):
88
+ """Get contacts by state."""
89
+ if not service:
90
+ return {"Error": "Data store service not set"}
91
+ contacts = service.get_contacts_by_state(state)
92
+ return {"contacts": contacts}
@@ -0,0 +1,12 @@
1
+ # Project Root Folder
2
+ from pathlib import Path
3
+
4
+ ROOT_DIR = Path(__file__).parents[0]
5
+ LOGGING_CONFIG_PATH = ROOT_DIR / "logging_configuration.conf"
6
+
7
+
8
+ # Constants
9
+ VCF_EXTENSION = ".vcf"
10
+ SAMPLE_CONTACTS_DIR = "tests/data"
11
+ SAMPLE_CONTACTS_FILE = "contacts.vcf"
12
+ FILE_DATA_STORE_SERVICE = "f"
@@ -0,0 +1,28 @@
1
+ [loggers]
2
+ keys=root
3
+
4
+ [handlers]
5
+ keys=consoleHandler,fileHandler
6
+
7
+ [formatters]
8
+ keys=outputFormatter
9
+
10
+ [logger_root]
11
+ level=DEBUG
12
+ handlers=fileHandler,consoleHandler
13
+
14
+ [handler_fileHandler]
15
+ class=handlers.TimedRotatingFileHandler
16
+ level=INFO
17
+ formatter=outputFormatter
18
+ args=('%(log_dir)s/contactlookup.log', 'midnight', 1, 0, 'utf-8')
19
+
20
+ [handler_consoleHandler]
21
+ class=StreamHandler
22
+ level=ERROR
23
+ formatter=outputFormatter
24
+ args=(sys.stdout,)
25
+
26
+ [formatter_outputFormatter]
27
+ format=%(asctime)s - %(name)s (%(filename)s:%(lineno)d): [%(levelname)s] %(message)s
28
+ datefmt=
File without changes
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass
5
+ class Address:
6
+ id: int | None = field(init=False, default=None)
7
+ street: str
8
+ city: str | None
9
+ state: str | None
10
+ postal_code: str | None
11
+ contact_id: int
12
+ type: str | None = None
13
+ country: str | None = None
14
+
15
+ def __post_init__(self):
16
+ self.street = self.street.strip()
17
+ self.city = self.city.strip().upper() if self.city is not None else None
18
+ self.state = self.state.strip().upper() if self.state is not None else None
19
+ self.postal_code = (
20
+ self.postal_code.strip() if self.postal_code is not None else None
21
+ )
22
+ self.country = (
23
+ self.country.strip().upper() if self.country is not None else None
24
+ )
25
+ self.type = self.type.strip().upper() if self.type is not None else None
@@ -0,0 +1,63 @@
1
+ # A VCF contact model
2
+ from dataclasses import dataclass, field
3
+
4
+ from contactlookup.models.address import Address
5
+ from contactlookup.models.email import Email
6
+ from contactlookup.models.phone_number import PhoneNumber
7
+
8
+
9
+ @dataclass
10
+ class Contact:
11
+ id: int
12
+ first_name: str
13
+ last_name: str
14
+ other_names: str | None
15
+ company: str | None
16
+ title: str | None
17
+ nickname: str | None = field(default=None)
18
+ birthday: str | None = field(default=None)
19
+ phone_numbers: list[PhoneNumber] = field(default_factory=list)
20
+ addresses: list[Address] = field(default_factory=list)
21
+ emails: list[Email] = field(default_factory=list)
22
+
23
+ def __post_init__(self):
24
+ self.first_name = self.first_name.strip().upper()
25
+ self.last_name = self.last_name.strip().upper()
26
+ self.company = self.company.strip() if self.company is not None else None
27
+ self.title = self.title.strip().upper() if self.title is not None else None
28
+ self.other_names = (
29
+ self.other_names.strip().upper() if self.other_names is not None else None
30
+ )
31
+ self.nickname = (
32
+ self.nickname.strip().upper() if self.nickname is not None else None
33
+ )
34
+
35
+ def __eq__(self, other):
36
+ if not isinstance(other, Contact):
37
+ return False
38
+
39
+ return (
40
+ self.first_name == other.first_name
41
+ and self.last_name == other.last_name
42
+ and self.other_names == other.other_names
43
+ and self.company == other.company
44
+ and self.title == other.title
45
+ and self.nickname == other.nickname
46
+ and self.phone_numbers == other.phone_numbers
47
+ and self.addresses == other.addresses
48
+ and self.emails == other.emails
49
+ )
50
+
51
+ def add_phone_number(self, phone_number: PhoneNumber):
52
+ self.phone_numbers.append(phone_number)
53
+
54
+ def add_address(self, address: Address):
55
+ self.addresses.append(address)
56
+
57
+ def add_email(self, email: Email):
58
+ self.emails.append(email)
59
+
60
+ def refresh(self):
61
+ # Run the post init method again to ensure all fields are properly
62
+ # formatted
63
+ self.__post_init__()
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass
5
+ class Email:
6
+ id: int | None = field(init=False, default=None)
7
+ email: str
8
+ type: str | None
9
+ contact_id: int
10
+
11
+ def __post_init__(self):
12
+ self.email = self.email.strip()
13
+ self.type = self.type.strip().lower() if self.type is not None else None
@@ -0,0 +1,16 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass
5
+ class PhoneNumber:
6
+ id: int | None = field(init=False, default=None)
7
+ number: str
8
+ contact_id: int
9
+ type: str | None = field(default=None)
10
+
11
+ def __post_init__(self):
12
+ # Remove all spaces, dashes, parentheses and + from the phone number
13
+ self.number = "".join(
14
+ [char for char in self.number if char.isnumeric()],
15
+ )
16
+ self.type = self.type.strip().upper() if self.type is not None else None
File without changes
@@ -0,0 +1,54 @@
1
+ """Abstract class for data store service."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+ from contactlookup.models.contact import Contact
6
+
7
+
8
+ class DataStoreService(ABC):
9
+ """Abstract class for data store service."""
10
+
11
+ @abstractmethod
12
+ def initialize(self) -> bool:
13
+ """Initialize data store."""
14
+
15
+ @abstractmethod
16
+ def get_contact(self, contact_id: int) -> Contact | None:
17
+ """Get contact by ID."""
18
+
19
+ @abstractmethod
20
+ def get_contacts(self) -> list[Contact]:
21
+ """Get all contacts."""
22
+
23
+ @abstractmethod
24
+ def get_contacts_by_fname(self, fname: str) -> list:
25
+ """Get contacts by first name."""
26
+
27
+ @abstractmethod
28
+ def get_contacts_by_phone_number(self, phone_number: str) -> list:
29
+ """Get contacts by phone number."""
30
+
31
+ @abstractmethod
32
+ def get_contacts_by_email(self, email: str) -> list:
33
+ """Get contacts by email."""
34
+
35
+ @abstractmethod
36
+ def get_contacts_by_country(self, country: str) -> list:
37
+ """Get contacts by country."""
38
+
39
+ @abstractmethod
40
+ def get_contacts_by_state(self, state: str) -> list:
41
+ """Get contacts by state."""
42
+
43
+ # Write operations are not needed for this project
44
+ # @abstractmethod
45
+ # def create_contact(self, contact: dict) -> dict:
46
+ # """Create a new contact."""
47
+
48
+ # @abstractmethod
49
+ # def update_contact(self, contact_id: str, contact: dict) -> dict:
50
+ # """Update contact by ID."""
51
+
52
+ # @abstractmethod
53
+ # def delete_contact(self, contact_id: str) -> dict:
54
+ # """Delete contact by ID."""