Jessica Walkenhorst commited on
Commit
fbc21be
β€’
1 Parent(s): 4ac520e

Feature/add UI (#6)

Browse files

* Add first version of streamlit ui
* Add a new test
* Add to Makefile

Makefile CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  isort:
2
  ./bin/run-isort.sh
3
 
@@ -7,7 +10,9 @@ flake8:
7
  black:
8
  ./bin/run-black.sh
9
 
 
 
10
  test:
11
  poetry run pytest
12
 
13
- prepare: isort black flake8 test
 
1
+ ui:
2
+ poetry run streamlit run ui.py
3
+
4
  isort:
5
  ./bin/run-isort.sh
6
 
 
10
  black:
11
  ./bin/run-black.sh
12
 
13
+ tidy: isort black flake8
14
+
15
  test:
16
  poetry run pytest
17
 
18
+ prepare: tidy test
app.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import streamlit as st
3
+
4
+ from maorganizer.datawrangling import Person
5
+
6
+ from maorganizer.ui import TASKS, render_xlsx_download_button, create_file_uploader, create_task_selector
7
+
8
+ st.title("πŸ“… Meeting Attendance Organizer")
9
+ st.markdown("This app fullfills a simple need: Take a list of names of people attending a meeting and peform one (or multiple) of the following tasks:")
10
+ st.markdown("""* βœ‚οΈ Split their names into first name and surname\n* πŸ‘€ Compare two lists with each other and see who is new on the second list\n * πŸ”Ž Find people in a list by either searching for their complete names or parts of their name\n * πŸ’Ύ Write any of the results back out, so you can share it with others""")
11
+
12
+ st.header("πŸ“‚ Step 1: Upload your Files")
13
+ st.markdown("Upload the file(s) containing your meeting attendees. The expected format is a single column containing the attendees' full names. If you column name is not Name, you will be able to specify the column name after uploading the data. Additional columns will be ignored.")
14
+
15
+ meetings = {}
16
+ meetings = create_file_uploader()
17
+
18
+ task = create_task_selector()
19
+
20
+ st.header("πŸ”₯ Step 3: Let's Go!")
21
+ st.subheader(f"You are going to ... {task}")
22
+ if not meetings:
23
+ st.info("⬆ You need to upload some data first ⬆")
24
+
25
+ if meetings:
26
+ if task == TASKS.SPLIT.value:
27
+
28
+ filename = st.selectbox("Choose a file πŸ“„", options=list(meetings.keys()), key=task)
29
+ #filename = render_file_selector(meetings, key=task)
30
+
31
+ render_xlsx_download_button({'Full list of Attendees': meetings[filename]},
32
+ filename=f"processed-attendees-{Path(filename).stem}.xlsx",
33
+ key=TASKS.SPLIT.value+'download')
34
+
35
+ elif task == TASKS.FIND.value:
36
+
37
+ filename = st.selectbox("πŸ“„ Choose a file", options=list(meetings.keys()), key=task)
38
+ attendees = meetings[filename]
39
+
40
+ textinput = st.text_input("πŸ”Ž Who are you looking for? If you are looking for more than one, separate them by comma.")
41
+ st.markdown("⚠️ By default, the algorithm will surface any names that contain your search terms as substrings (e.g. if you search for Jon, it will surface both Jon and Jonathan). Tick the box below in case you want to display only entries where one of the names matches your search string exactly.")
42
+ st.markdown("⚠️⚠️ Note that either way the search is case-insensitive.")
43
+ exact_match = st.checkbox("Find Exact Name")
44
+
45
+ if textinput.strip():
46
+ st.header("Search Results")
47
+ if exact_match:
48
+ people_to_find = [Person(word.strip()) for word in textinput.split(',')]
49
+
50
+ for to_find, found in attendees.find_people(people_to_find).items():
51
+ st.subheader(f"**{to_find.name}**")
52
+ st.markdown(f"{', '.join([p.name for p in found]) if found else 'Sorry, none found.'}")
53
+ else:
54
+ words_to_find = [word.strip() for word in textinput.split(',')]
55
+
56
+ for word_to_find, people_found in attendees.find_words(words_to_find).items():
57
+ st.subheader(f"**{word_to_find}**")
58
+ st.markdown(f"{', '.join([p.name for p in people_found]) if people_found else 'Sorry, none found.'}")
59
+
60
+ elif task == TASKS.COMPARE.value:
61
+ col1, col2 = st.columns(2)
62
+ with col1:
63
+ filename_old = st.selectbox("Choose your original file", options=list(meetings.keys()), key=task)
64
+ with col2:
65
+ filename_new = st.selectbox("Choose your updated file", options=set(meetings.keys()) - {filename_old})
66
+
67
+ listcomparison = (
68
+ {'Original List': meetings[filename_old],
69
+ 'Updated List - Full': meetings[filename_new],
70
+ 'Updated List - Only Updates': meetings[filename_old].update(meetings[filename_new])})
71
+
72
+ render_xlsx_download_button(listcomparison, filename=f"{Path(filename_old).stem}-updated.xlsx", key=TASKS.COMPARE.value+'download')
poetry.lock CHANGED
@@ -308,6 +308,14 @@ category = "main"
308
  optional = false
309
  python-versions = ">=3.6"
310
 
 
 
 
 
 
 
 
 
311
  [[package]]
312
  name = "exceptiongroup"
313
  version = "1.1.0"
@@ -1089,6 +1097,17 @@ category = "main"
1089
  optional = false
1090
  python-versions = ">=3.8"
1091
 
 
 
 
 
 
 
 
 
 
 
 
1092
  [[package]]
1093
  name = "packaging"
1094
  version = "23.0"
@@ -1865,7 +1884,7 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools"
1865
  [metadata]
1866
  lock-version = "1.1"
1867
  python-versions = "^3.10"
1868
- content-hash = "4994be222a316473241cb32a1ada1e927f3cd5f6e9b6afad67bd2e79a07180c7"
1869
 
1870
  [metadata.files]
1871
  aiofiles = [
@@ -2181,6 +2200,10 @@ entrypoints = [
2181
  {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"},
2182
  {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"},
2183
  ]
 
 
 
 
2184
  exceptiongroup = [
2185
  {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
2186
  {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
@@ -2456,6 +2479,10 @@ numpy = [
2456
  {file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"},
2457
  {file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"},
2458
  ]
 
 
 
 
2459
  packaging = [
2460
  {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
2461
  {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
 
308
  optional = false
309
  python-versions = ">=3.6"
310
 
311
+ [[package]]
312
+ name = "et-xmlfile"
313
+ version = "1.1.0"
314
+ description = "An implementation of lxml.xmlfile for the standard library"
315
+ category = "main"
316
+ optional = false
317
+ python-versions = ">=3.6"
318
+
319
  [[package]]
320
  name = "exceptiongroup"
321
  version = "1.1.0"
 
1097
  optional = false
1098
  python-versions = ">=3.8"
1099
 
1100
+ [[package]]
1101
+ name = "openpyxl"
1102
+ version = "3.1.2"
1103
+ description = "A Python library to read/write Excel 2010 xlsx/xlsm files"
1104
+ category = "main"
1105
+ optional = false
1106
+ python-versions = ">=3.6"
1107
+
1108
+ [package.dependencies]
1109
+ et-xmlfile = "*"
1110
+
1111
  [[package]]
1112
  name = "packaging"
1113
  version = "23.0"
 
1884
  [metadata]
1885
  lock-version = "1.1"
1886
  python-versions = "^3.10"
1887
+ content-hash = "9857bd72535987e589103fdfd65fcf409ea02207f0c0d0bfc62531770c009a0f"
1888
 
1889
  [metadata.files]
1890
  aiofiles = [
 
2200
  {file = "entrypoints-0.4-py3-none-any.whl", hash = "sha256:f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f"},
2201
  {file = "entrypoints-0.4.tar.gz", hash = "sha256:b706eddaa9218a19ebcd67b56818f05bb27589b1ca9e8d797b74affad4ccacd4"},
2202
  ]
2203
+ et-xmlfile = [
2204
+ {file = "et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"},
2205
+ {file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"},
2206
+ ]
2207
  exceptiongroup = [
2208
  {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"},
2209
  {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"},
 
2479
  {file = "numpy-1.24.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f"},
2480
  {file = "numpy-1.24.2.tar.gz", hash = "sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22"},
2481
  ]
2482
+ openpyxl = [
2483
+ {file = "openpyxl-3.1.2-py2.py3-none-any.whl", hash = "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5"},
2484
+ {file = "openpyxl-3.1.2.tar.gz", hash = "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184"},
2485
+ ]
2486
  packaging = [
2487
  {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"},
2488
  {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"},
pyproject.toml CHANGED
@@ -9,6 +9,7 @@ readme = "README.md"
9
  python = "^3.10"
10
  pandas = "^1.5.3"
11
  streamlit = "^1.17.0"
 
12
 
13
  [tool.poetry.group.dev.dependencies]
14
  black = "^23.1.0"
 
9
  python = "^3.10"
10
  pandas = "^1.5.3"
11
  streamlit = "^1.17.0"
12
+ openpyxl = "^3.1.2"
13
 
14
  [tool.poetry.group.dev.dependencies]
15
  black = "^23.1.0"
src/maorganizer/datawrangling.py CHANGED
@@ -7,9 +7,7 @@ import pandas as pd
7
 
8
  DATAFOLDER = Path().cwd() / "data"
9
 
10
- MONTH = "Feb"
11
-
12
- FILENAME = f"participants-Meetup-{MONTH}"
13
 
14
 
15
  @dataclass
@@ -17,7 +15,9 @@ class Person:
17
  name: str
18
 
19
  def __post_init__(self):
20
- self.name = ' '.join([namepart for namepart in self.name.strip().title().split(' ') if namepart])
 
 
21
 
22
  def __hash__(self):
23
  return hash(self.name)
@@ -38,27 +38,31 @@ class Person:
38
  def lastname(self):
39
  return " ".join(self.name.split(" ")[1:])
40
 
 
 
 
41
 
42
  @dataclass
43
  class Attendancelist:
44
  participants: Set[Person]
45
 
 
 
 
46
  def load_from_file(
47
- filename: pathlib.PosixPath, cname: str = "Name", sep: str = None
48
  ):
49
- if sep:
50
- df = pd.read_csv(filename, sep=sep)
51
- elif filename.suffix in [".xlsx", ".xls"]:
52
  df = pd.read_excel(filename)
53
  elif filename.suffix == ".csv":
54
- df = pd.read_csv(filename)
55
  else:
56
  raise ValueError(
57
  "Unsupported filetype, please specify a separator or choose one "
58
  "of the following filetypes: .xlsx, .xls, .csv"
59
  )
60
 
61
- return Attendancelist({Person(name) for name in df[cname]})
62
 
63
  @property
64
  def n_attendees(self):
@@ -82,15 +86,21 @@ class Attendancelist:
82
  raise ValueError(
83
  "Unsupported filetype, please choose one of the following: .xlsx, .csv"
84
  )
85
-
86
  def to_file(self) -> str:
87
- return self.to_df().to_csv(index=False).encode('utf-8')
88
 
89
  def update(self, other: "Attendancelist"):
90
  return Attendancelist(other.participants - self.participants)
91
 
92
- def find(self, somebody: Person):
93
  return {p for p in self.participants if p.is_similar(somebody)}
94
 
95
- def find_multiple(self, people: List[Person]):
96
- return {p: self.find(p) for p in people}
 
 
 
 
 
 
 
7
 
8
  DATAFOLDER = Path().cwd() / "data"
9
 
10
+ NAMECOLUMN = "Name"
 
 
11
 
12
 
13
  @dataclass
 
15
  name: str
16
 
17
  def __post_init__(self):
18
+ self.name = " ".join(
19
+ [namepart for namepart in self.name.strip().title().split(" ") if namepart]
20
+ )
21
 
22
  def __hash__(self):
23
  return hash(self.name)
 
38
  def lastname(self):
39
  return " ".join(self.name.split(" ")[1:])
40
 
41
+ def name_contains(self, text) -> bool:
42
+ return text in self.name.lower()
43
+
44
 
45
  @dataclass
46
  class Attendancelist:
47
  participants: Set[Person]
48
 
49
+ def load_from_df(df, cname: str = NAMECOLUMN):
50
+ return Attendancelist({Person(name) for name in df[cname]})
51
+
52
  def load_from_file(
53
+ filename: pathlib.PosixPath, cname: str = NAMECOLUMN, sep: str = None
54
  ):
55
+ if filename.suffix in [".xlsx", ".xls"]:
 
 
56
  df = pd.read_excel(filename)
57
  elif filename.suffix == ".csv":
58
+ df = pd.read_csv(filename, sep=sep)
59
  else:
60
  raise ValueError(
61
  "Unsupported filetype, please specify a separator or choose one "
62
  "of the following filetypes: .xlsx, .xls, .csv"
63
  )
64
 
65
+ return Attendancelist.load_from_df(df, cname)
66
 
67
  @property
68
  def n_attendees(self):
 
86
  raise ValueError(
87
  "Unsupported filetype, please choose one of the following: .xlsx, .csv"
88
  )
89
+
90
  def to_file(self) -> str:
91
+ return self.to_df().to_csv(index=False).encode("utf-8")
92
 
93
  def update(self, other: "Attendancelist"):
94
  return Attendancelist(other.participants - self.participants)
95
 
96
+ def find_person(self, somebody: Person):
97
  return {p for p in self.participants if p.is_similar(somebody)}
98
 
99
+ def find_people(self, people: List[Person]):
100
+ return {p: self.find_person(p) for p in people}
101
+
102
+ def find_word(self, word: str):
103
+ return {p for p in self.participants if p.name_contains(word.lower())}
104
+
105
+ def find_words(self, words: List[str]):
106
+ return {word: self.find_word(word) for word in words}
src/maorganizer/ui.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ from enum import Enum
3
+ from pathlib import Path
4
+ from typing import Dict, Tuple
5
+
6
+ import pandas as pd
7
+ import streamlit as st
8
+
9
+ from maorganizer.datawrangling import NAMECOLUMN, Attendancelist
10
+
11
+ CSV_EXTENSIONS = [".csv", ".txt"]
12
+ EXCEL_EXTENSIONS = [".xls", ".xlsx"]
13
+ ACCEPTED_EXTENSIONS = CSV_EXTENSIONS + EXCEL_EXTENSIONS
14
+
15
+
16
+ SEPARATORTYPES = {"TAB": "\t", "COMMA": ","}
17
+
18
+
19
+ class TASKS(str, Enum):
20
+ SPLIT = "βœ‚οΈ ... split attendees into first and last name and download results"
21
+ COMPARE = " πŸ‘€ ... compare two meetings with each other and find updates"
22
+ FIND = " πŸ”Ž ... find specific attendees"
23
+
24
+ def __str__(self) -> str: # makes enum values duck-type to strings
25
+ return str.__str__(self)
26
+
27
+
28
+ def load_df_from_uploaded_data(filename, data, sep=None) -> pd.DataFrame:
29
+ if Path(filename).suffix in EXCEL_EXTENSIONS:
30
+ df = pd.read_excel(data)
31
+ elif Path(filename).suffix in CSV_EXTENSIONS:
32
+ df = pd.read_csv(filename, sep=sep)
33
+ else:
34
+ raise ValueError(
35
+ f"Please choose one of the following extensions: {', '.join(ACCEPTED_EXTENSIONS)}"
36
+ )
37
+ return df
38
+
39
+
40
+ def make_attendance_data_from_file_uploads(
41
+ uploaded_files, sep=None, cname=NAMECOLUMN
42
+ ) -> Dict:
43
+ return {
44
+ file.name: Attendancelist.load_from_df(
45
+ load_df_from_uploaded_data(file.name, file, sep), cname=cname
46
+ )
47
+ for file in uploaded_files
48
+ }
49
+
50
+
51
+ def load_data(uploaded_files) -> Tuple[Dict, bool]:
52
+ try:
53
+ data = make_attendance_data_from_file_uploads(
54
+ uploaded_files, sep=None, cname=NAMECOLUMN
55
+ )
56
+ except KeyError:
57
+ contains_csvs = sum(
58
+ [Path(file.name).suffix in CSV_EXTENSIONS for file in uploaded_files]
59
+ )
60
+ if contains_csvs:
61
+ separator = st.radio(
62
+ "We detected text files in your input. What is their separator?",
63
+ sorted(SEPARATORTYPES.keys()),
64
+ )
65
+
66
+ namecolumn = st.text_input(
67
+ "Column header of your file's name column", NAMECOLUMN
68
+ )
69
+ try:
70
+ data = make_attendance_data_from_file_uploads(
71
+ uploaded_files, sep=SEPARATORTYPES[separator], cname=namecolumn
72
+ )
73
+ except KeyError:
74
+ st.error(
75
+ f"We could not find a column {namecolumn} in your data. Please use the options above to specify your column separator and the column name of your name column."
76
+ )
77
+ data = {}
78
+
79
+ if data:
80
+ st.success(
81
+ "Successfully loaded the following files:\\\n\\\n"
82
+ + "\\\n".join([f"{k} - {v.n_attendees} attendees" for k, v in data.items()])
83
+ )
84
+ return data
85
+
86
+
87
+ def render_file_selector(meetings, key):
88
+ show_processed_list = st.checkbox("Display the processed list of attendees")
89
+ if show_processed_list:
90
+ filename = st.selectbox(
91
+ "Select the file to display", options=list(meetings.keys()), key=key
92
+ )
93
+ attendees = meetings[filename]
94
+
95
+ st.write(attendees.to_df())
96
+
97
+
98
+ def create_file_uploader():
99
+ uploaded_files = st.file_uploader(
100
+ label="πŸ“„ Upload your files", accept_multiple_files=True
101
+ )
102
+ if uploaded_files:
103
+ meetings = load_data(uploaded_files)
104
+ render_file_selector(meetings, "file_upload")
105
+ return meetings
106
+ else:
107
+ return {}
108
+
109
+
110
+ def create_task_selector():
111
+ st.header("πŸ“ Step 2: Choose a Task")
112
+ task = st.radio("I would like to ...", [task.value for task in TASKS])
113
+
114
+ if task == TASKS.SPLIT.value:
115
+ st.markdown("❔ **Description:** Split a list of names into first and surname.")
116
+ elif task == TASKS.COMPARE.value:
117
+ st.markdown(
118
+ "❔ **Description:** Compare two attendee lists with each and find attendees who have recently joined."
119
+ )
120
+ elif task == TASKS.FIND.value:
121
+ st.markdown(
122
+ "❔ **Description:** Find attendees in a list by either first name or surname or by substrings."
123
+ )
124
+ return task
125
+
126
+
127
+ def render_xlsx_download_button(data, filename, key) -> None:
128
+ with io.BytesIO() as output:
129
+ with pd.ExcelWriter(output, engine="openpyxl") as writer:
130
+ for sheetname, attendees in data.items():
131
+ attendees.to_df().to_excel(writer, sheet_name=sheetname, index=False)
132
+ writer.save()
133
+
134
+ st.download_button(
135
+ label="πŸ’Ύ Download Results",
136
+ data=output.getvalue(),
137
+ file_name=filename,
138
+ mime="application/vnd.ms-excel",
139
+ key=key,
140
+ )
tests/test_attendancelist.py CHANGED
@@ -2,21 +2,30 @@ from maorganizer.datawrangling import Attendancelist, Person
2
 
3
 
4
  def test_attendancelist_finds_person_by_substring():
5
- assert Attendancelist({Person("zaphod beeblebrox"), Person("ford prefix")}).find(
6
- Person("zaphod")
7
- ) == {Person("Zaphod Beeblebrox")}
 
 
 
 
 
 
8
 
9
 
10
  def test_attendancelists_finds_multiple_people_if_existent():
11
  assert Attendancelist(
12
  {Person("zaphod beeblebrox"), Person("zaphod prefix"), Person("ford prefix")}
13
- ).find(Person("zaphod")) == {Person("Zaphod Beeblebrox"), Person("Zaphod Prefix")}
 
 
 
14
 
15
 
16
- def test_find_multiple_finds_alls():
17
  assert Attendancelist(
18
  {Person("zaphod beeblebrox"), Person("ford prefix"), Person("Marvin")}
19
- ).find_multiple({Person("zaphod"), Person("ford prefix")}) == {
20
  Person(name="Ford Prefix"): {Person(name="Ford Prefix")},
21
  Person(name="Zaphod"): {Person(name="Zaphod Beeblebrox")},
22
  }
 
2
 
3
 
4
  def test_attendancelist_finds_person_by_substring():
5
+ assert Attendancelist(
6
+ {Person("zaphod beeblebrox"), Person("ford prefix")}
7
+ ).find_word("aph") == {Person("Zaphod Beeblebrox")}
8
+
9
+
10
+ def test_attendancelist_finds_person_by_namepart():
11
+ assert Attendancelist(
12
+ {Person("zaphod beeblebrox"), Person("ford prefix")}
13
+ ).find_person(Person("zaphod")) == {Person("Zaphod Beeblebrox")}
14
 
15
 
16
  def test_attendancelists_finds_multiple_people_if_existent():
17
  assert Attendancelist(
18
  {Person("zaphod beeblebrox"), Person("zaphod prefix"), Person("ford prefix")}
19
+ ).find_person(Person("zaphod")) == {
20
+ Person("Zaphod Beeblebrox"),
21
+ Person("Zaphod Prefix"),
22
+ }
23
 
24
 
25
+ def test_find_people_finds_alls():
26
  assert Attendancelist(
27
  {Person("zaphod beeblebrox"), Person("ford prefix"), Person("Marvin")}
28
+ ).find_people({Person("zaphod"), Person("ford prefix")}) == {
29
  Person(name="Ford Prefix"): {Person(name="Ford Prefix")},
30
  Person(name="Zaphod"): {Person(name="Zaphod Beeblebrox")},
31
  }
tests/test_datawrangling.py CHANGED
@@ -40,4 +40,4 @@ def test_whitespace_gets_deleted_from_edges_of_name():
40
 
41
 
42
  def test_multiple_whitespace_gets_correctly_deleted_from_inside_a_name():
43
- assert Person("Zaphod Beeblebrox") == Person("Zaphod Beeblebrox")
 
40
 
41
 
42
  def test_multiple_whitespace_gets_correctly_deleted_from_inside_a_name():
43
+ assert Person("Zaphod Beeblebrox") == Person("Zaphod Beeblebrox")