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.
- contactlookup/__init__.py +15 -0
- contactlookup/__main__.py +141 -0
- contactlookup/controller.py +92 -0
- contactlookup/definitions.py +12 -0
- contactlookup/logging_configuration.conf +28 -0
- contactlookup/models/__init__.py +0 -0
- contactlookup/models/address.py +25 -0
- contactlookup/models/contact.py +63 -0
- contactlookup/models/email.py +13 -0
- contactlookup/models/phone_number.py +16 -0
- contactlookup/services/__init__.py +0 -0
- contactlookup/services/data_store_service.py +54 -0
- contactlookup/services/file_data_store_service.py +429 -0
- contactlookup-1.0.2.dist-info/LICENSE +674 -0
- contactlookup-1.0.2.dist-info/METADATA +269 -0
- contactlookup-1.0.2.dist-info/RECORD +19 -0
- contactlookup-1.0.2.dist-info/WHEEL +4 -0
- contactlookup-1.0.2.dist-info/entry_points.txt +3 -0
- tests/data/contacts.vcf +53 -0
|
@@ -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."""
|