themefinder 0.7.0__py3-none-any.whl → 0.7.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.

Potentially problematic release.


This version of themefinder might be problematic. Click here for more details.

themefinder/core.py CHANGED
@@ -186,7 +186,7 @@ async def theme_generation(
186
186
  llm: RunnableWithFallbacks,
187
187
  question: str,
188
188
  batch_size: int = 50,
189
- partition_key: str | None = "position",
189
+ partition_key: str | None = None,
190
190
  prompt_template: str | Path | PromptTemplate = "theme_generation",
191
191
  system_prompt: str = CONSULTATION_SYSTEM_PROMPT,
192
192
  concurrency: int = 10,
@@ -317,6 +317,7 @@ def theme_clustering(
317
317
  target_themes: int = 10,
318
318
  significance_percentage: float = 10.0,
319
319
  return_all_themes: bool = False,
320
+ system_prompt: str = CONSULTATION_SYSTEM_PROMPT,
320
321
  ) -> tuple[pd.DataFrame, pd.DataFrame]:
321
322
  """Perform hierarchical clustering of themes using an agentic approach.
322
323
 
@@ -340,6 +341,8 @@ def theme_clustering(
340
341
  selecting significant themes. Defaults to 10.0.
341
342
  return_all_themes (bool, optional): If True, returns all clustered themes.
342
343
  If False, returns only significant themes. Defaults to False.
344
+ system_prompt (str): System prompt to guide the LLM's behavior.
345
+ Defaults to CONSULTATION_SYSTEM_PROMPT.
343
346
 
344
347
  Returns:
345
348
  tuple[pd.DataFrame, pd.DataFrame]:
@@ -362,7 +365,10 @@ def theme_clustering(
362
365
 
363
366
  # Initialize clustering agent with structured output LLM
364
367
  agent = ThemeClusteringAgent(
365
- llm.with_structured_output(HierarchicalClusteringResponse), initial_themes
368
+ llm.with_structured_output(HierarchicalClusteringResponse),
369
+ initial_themes,
370
+ system_prompt,
371
+ target_themes,
366
372
  )
367
373
 
368
374
  # Perform clustering
@@ -444,6 +450,32 @@ async def theme_refinement(
444
450
  system_prompt=system_prompt,
445
451
  concurrency=concurrency,
446
452
  )
453
+
454
+ def assign_sequential_topic_ids(df: pd.DataFrame) -> pd.DataFrame:
455
+ """
456
+ Assigns sequential alphabetic topic_ids (A, B, ..., Z, AA, AB, ...) to the DataFrame.
457
+ """
458
+
459
+ def alpha_ids(n: int) -> list[str]:
460
+ ids = []
461
+ for i in range(n):
462
+ s = ""
463
+ x = i
464
+ while True:
465
+ x, r = divmod(x, 26)
466
+ s = chr(65 + r) + s
467
+ if x == 0:
468
+ break
469
+ x -= 1
470
+ ids.append(s)
471
+ return ids
472
+
473
+ if not df.empty:
474
+ df["topic_id"] = alpha_ids(len(df))
475
+ return df
476
+
477
+ refined_themes = assign_sequential_topic_ids(refined_themes)
478
+
447
479
  return refined_themes, _
448
480
 
449
481
 
themefinder/models.py CHANGED
@@ -217,9 +217,6 @@ class ThemeCondensationResponses(ValidatedModel):
217
217
  class RefinedTheme(ValidatedModel):
218
218
  """Model for a single refined theme"""
219
219
 
220
- topic_id: str = Field(
221
- ..., description="Single uppercase letter ID (A-Z, then AA, AB, etc.)"
222
- )
223
220
  topic: str = Field(
224
221
  ..., description="Topic label and description combined with a colon separator"
225
222
  )
@@ -231,19 +228,9 @@ class RefinedTheme(ValidatedModel):
231
228
  def run_validations(self) -> "RefinedTheme":
232
229
  """Run all validations for RefinedTheme"""
233
230
  self.validate_non_empty_fields()
234
- self.validate_topic_id_format()
235
231
  self.validate_topic_format()
236
232
  return self
237
233
 
238
- def validate_topic_id_format(self) -> "RefinedTheme":
239
- """
240
- Validate that topic_id follows the expected format (A-Z, then AA, AB, etc.).
241
- """
242
- topic_id = self.topic_id.strip()
243
- if not topic_id.isupper() or not topic_id.isalpha():
244
- raise ValueError(f"topic_id must be uppercase letters only: {topic_id}")
245
- return self
246
-
247
234
  def validate_topic_format(self) -> "RefinedTheme":
248
235
  """
249
236
  Validate that topic contains a label and description separated by a colon.
@@ -273,9 +260,6 @@ class ThemeRefinementResponses(ValidatedModel):
273
260
  def run_validations(self) -> "ThemeRefinementResponses":
274
261
  """Ensure there are no duplicate themes"""
275
262
  self.validate_non_empty_fields()
276
- topic_ids = [theme.topic_id for theme in self.responses]
277
- if len(topic_ids) != len(set(topic_ids)):
278
- raise ValueError("Duplicate topic_ids detected")
279
263
  topics = [theme.topic.lower().strip() for theme in self.responses]
280
264
  if len(topics) != len(set(topics)):
281
265
  raise ValueError("Duplicate topics detected")
@@ -288,10 +272,6 @@ class ThemeMappingOutput(ValidatedModel):
288
272
 
289
273
  response_id: int = Field(gt=0, description="Response ID, must be greater than 0")
290
274
  labels: List[str] = Field(..., description="List of theme labels")
291
- reasons: List[str] = Field(..., description="List of reasons for mapping")
292
- stances: List[Stance] = Field(
293
- ..., description="List of stances (POSITIVE or NEGATIVE)"
294
- )
295
275
 
296
276
  @model_validator(mode="after")
297
277
  def run_validations(self) -> "ThemeMappingOutput":
@@ -299,7 +279,6 @@ class ThemeMappingOutput(ValidatedModel):
299
279
  Run all validations for ThemeMappingOutput.
300
280
  """
301
281
  self.validate_non_empty_fields()
302
- self.validate_equal_lengths("stances", "labels", "reasons")
303
282
  self.validate_unique_items("labels")
304
283
  return self
305
284
 
@@ -1,3 +1,5 @@
1
+ {system_prompt}
2
+
1
3
  Analyze these topics and identify which ones should be merged based on semantic similarity.
2
4
  Your goal is to significantly reduce the number of topics by creating meaningful parent topics.
3
5
  Be aggressive in finding opportunities to merge topics that share any semantic relationship.
@@ -22,10 +24,11 @@ Guidelines:
22
24
  - source_topic_count must be the sum of all child topic counts
23
25
  - children must be a list of valid topic_ids from the input
24
26
  - should_terminate should only be true if ALL of these conditions are met:
25
- * There are fewer than 10 active topics remaining
27
+ * There are fewer than {target_themes} active topics remaining
26
28
  * The remaining topics are fundamentally incompatible semantically
27
29
  * Any further merging would create meaninglessly broad categories
28
30
 
29
31
  If no topics should be merged in this iteration but future iterations might still yield meaningful merges, set should_terminate to false with an empty parent_themes list.
32
+ If no topics should be merged and the termination conditions are met, set should_terminate to true with an empty parent_themes list.
30
33
 
31
- If no topics should be merged and the termination conditions are met, set should_terminate to true with an empty parent_themes list.
34
+ N.B. Under no circumstances should you create a parent theme with a single child. You do not need to return all of the original themes, if they don't belong to a newly created parent feel free to omit them.
@@ -4,11 +4,23 @@ You will receive a list of RESPONSES, each containing a response_id and a respon
4
4
  Your job is to analyze each response to the QUESTION below and decide if a response contains rich evidence.
5
5
  You MUST include every response ID in the output.
6
6
 
7
- Evidence-rich responses contain one or more of the following:
8
- - Specific facts or figures that shed new light on the issue (e.g., statistics, percentages, measurements, dates)
9
- - Concrete examples and specific insights that could inform decision-making
10
- - Detailed personal or professional experiences with clear contextual information or specific incidents
11
- In addition to the above an evidence rich response should answer the question and provide deeper insights than an average response.
7
+ A response is evidence-rich only if it satisfies both of the following:
8
+
9
+ Relevance and depth:
10
+     - It clearly answers the question
11
+     - AND provides insights that go beyond generic opinion, such as nuanced reasoning, contextual explanation, or argumentation that could inform decision-making
12
+
13
+ Substantive evidence, including at least one of:
14
+     - Specific, verifiable facts or data (e.g., statistics, dates, named reports or studies)
15
+     - Concrete, illustrative examples that clearly support a broader claim
16
+     - Detailed personal or professional experiences that include contextual information (e.g., roles, locations, timelines)
17
+
18
+ Do NOT classify a response as evidence-rich if it:
19
+ - Uses vague or general language with no supporting detail
20
+ - Restates commonly known points without adding new information
21
+ - Shares personal anecdotes without sufficient context or a clear takeaway
22
+
23
+ Before answering, ask: Would this response provide useful input to someone drafting policy, beyond what is already commonly known or expected?
12
24
 
13
25
  For each response, determine:
14
26
  EVIDENCE_RICH - does the response contain significant evidence as defined above?
@@ -1,11 +1,15 @@
1
1
  {system_prompt}
2
2
 
3
- Below is a question and a list of topics extracted from answers to that question. Each topic has a topic_label, topic_description, and may have a source_topic_count field indicating how many original topics it represents.
3
+ Below is a question and a list of topics extracted from answers to that question.
4
+
5
+ This list contains a large number of duplicate and redundant topics that present the same concept with different phrasing.
6
+
7
+ Each topic has a topic_label, topic_description, and may have a source_topic_count field indicating how many original topics it represents.
4
8
 
5
9
  Your task is to analyze these topics and produce a refined list that:
6
- 1. Identifies and preserves core themes that appear frequently
7
- 2. Combines redundant topics while maintaining nuanced differences
8
- 3. Ensures the final list represents the full spectrum of viewpoints present in the original data
10
+ 1. Significantly reduces the total number of topics
11
+ 2. Identifies and preserves core themes that appear frequently
12
+ 3. Combines redundant topics
9
13
  4. Tracks the total number of original topics combined into each new topic
10
14
 
11
15
  Guidelines for Topic Analysis:
@@ -16,10 +16,6 @@ Your task is to analyze each response and decide which topics are present. Guide
16
16
  - Each response can be assigned to multiple topics if it matches more than one topic from the TOPIC LIST.
17
17
  - Each topic can only be assigned once per response, if the topic is mentioned more than once use the first mention for reasoning and stance.
18
18
  - There is no limit on how many topics can be assigned to a response.
19
- - For each assignment provide a single rationale for why you have chosen the label.
20
- - For each topic identified in a response, indicate whether the response expresses a positive or negative stance toward that topic (options: 'POSITIVE' or 'NEGATIVE')
21
- - You MUST use either 'POSITIVE' or 'NEGATIVE'
22
- - The order of reasons and stances must align with the order of labels (e.g., stance_a applies to topic_a)
23
19
 
24
20
  You MUST include every response ID in the output.
25
21
  If the response can not be labelled return empty sections where appropriate but you MUST return an entry
@@ -7,10 +7,9 @@ You will receive a list of TOPICS. These topics explicitly tie opinions to wheth
7
7
 
8
8
  ## Output
9
9
  You will produce a list of CLEAR STANCE TOPICS based on the input. Each topic should have four parts:
10
- 1. A topic_id that is an uppercase letter (starting from 'A', for the 27th element use AA)
11
- 2. A brief, clear topic label (3-7 words)
12
- 3. A more detailed topic description (1-2 sentences)
13
- 4. The source_topic_count field should be included for each topic and should reflect the number of original source topics that were merged to create this refined topic. If multiple source topics were combined, sum their individual counts. If only one source topic was used, simply retain its original count value.
10
+ 1. A brief, clear topic label (3-7 words)
11
+ 2. A more detailed topic description (1-2 sentences)
12
+ 3. The source_topic_count field should be included for each topic and should reflect the number of original source topics that were merged to create this refined topic. If multiple source topics were combined, sum their individual counts. If only one source topic was used, simply retain its original count value.
14
13
 
15
14
 
16
15
  ## Guidelines
@@ -46,11 +45,10 @@ You will produce a list of CLEAR STANCE TOPICS based on the input. Each topic sh
46
45
  2. Group closely related topics together.
47
46
  3. For each group or individual topic:
48
47
  a. Distill the core concept, removing any bias or opinion.
49
- b. Create a neutral, concise topic label.
48
+ b. Create a concise topic label.
50
49
  c. Write a more detailed description that provides context without taking sides.
51
50
  4. Review the entire list to ensure distinctiveness and adjust as needed.
52
- 5. Assign each output topic a topic_id that is an uppercase letter (starting from 'A', for the 27th element use AA)
53
- 6. Combine the topic label and description with a colon separator
51
+ 5. Combine the topic label and description with a colon separator
54
52
 
55
53
  TOPICS:
56
54
  {responses}
@@ -22,6 +22,8 @@ from .models import ThemeNode
22
22
  from .llm_batch_processor import load_prompt_from_file
23
23
  from .themefinder_logging import logger
24
24
 
25
+ CONSULTATION_SYSTEM_PROMPT = load_prompt_from_file("consultation_system_prompt")
26
+
25
27
 
26
28
  class ThemeClusteringAgent:
27
29
  """Agent for performing hierarchical clustering of topics using language models.
@@ -37,13 +39,21 @@ class ThemeClusteringAgent:
37
39
  current_iteration: Current iteration number in the clustering process
38
40
  """
39
41
 
40
- def __init__(self, llm: Runnable, themes: List[ThemeNode]) -> None:
42
+ def __init__(
43
+ self,
44
+ llm: Runnable,
45
+ themes: List[ThemeNode],
46
+ system_prompt: str = CONSULTATION_SYSTEM_PROMPT,
47
+ target_themes: int = 10,
48
+ ) -> None:
41
49
  """Initialize the clustering agent with an LLM and initial themes.
42
50
 
43
51
  Args:
44
52
  llm: Language model instance configured with structured output
45
53
  for HierarchicalClusteringResponse
46
54
  themes: List of ThemeNode objects to be clustered
55
+ system_prompt: System prompt to guide the LLM's behavior
56
+ target_themes: Target number of themes to cluster down to (default 10)
47
57
  """
48
58
  self.llm = llm
49
59
  self.themes: Dict[str, ThemeNode] = {}
@@ -51,6 +61,8 @@ class ThemeClusteringAgent:
51
61
  self.themes[theme.topic_id] = theme
52
62
  self.active_themes = set(self.themes.keys())
53
63
  self.current_iteration = 0
64
+ self.system_prompt = system_prompt
65
+ self.target_themes = target_themes
54
66
 
55
67
  def _format_prompt(self) -> str:
56
68
  """Format the clustering prompt with current active themes.
@@ -74,7 +86,10 @@ class ThemeClusteringAgent:
74
86
  # Load the clustering prompt template
75
87
  prompt_template = load_prompt_from_file("agentic_theme_clustering")
76
88
  return prompt_template.format(
77
- themes_json=themes_json, iteration=self.current_iteration
89
+ themes_json=themes_json,
90
+ iteration=self.current_iteration,
91
+ system_prompt=self.system_prompt,
92
+ target_themes=self.target_themes,
78
93
  )
79
94
 
80
95
  @retry(
@@ -102,11 +117,20 @@ class ThemeClusteringAgent:
102
117
  """
103
118
  prompt = self._format_prompt()
104
119
  response = self.llm.invoke(prompt)
105
- # The response is already a parsed dictionary when using with_structured_output
106
- result = response
107
- for i, parent in enumerate(result["parent_themes"]):
108
- new_theme_id = f"{chr(65 + i)}_{self.current_iteration}"
109
- children = [c for c in parent["children"] if c in self.active_themes]
120
+ for i, parent in enumerate(response.parent_themes):
121
+
122
+ def to_alpha(idx: int) -> str:
123
+ """Convert 0-based integer to Excel-style column name (A, B, ..., Z, AA, AB, ...) without divmod."""
124
+ idx += 1 # 1-based for Excel logic
125
+ result = []
126
+ while idx > 0:
127
+ rem = (idx - 1) % 26
128
+ result.append(chr(65 + rem))
129
+ idx = (idx - 1) // 26
130
+ return "".join(reversed(result))
131
+
132
+ new_theme_id = f"{to_alpha(i)}_{self.current_iteration}"
133
+ children = [c for c in parent.children if c in self.active_themes]
110
134
  for child in children:
111
135
  self.themes[child].parent_id = new_theme_id
112
136
  total_source_count = sum(
@@ -114,8 +138,8 @@ class ThemeClusteringAgent:
114
138
  )
115
139
  new_theme = ThemeNode(
116
140
  topic_id=new_theme_id,
117
- topic_label=parent["topic_label"],
118
- topic_description=parent["topic_description"],
141
+ topic_label=parent.topic_label,
142
+ topic_description=parent.topic_description,
119
143
  source_topic_count=total_source_count,
120
144
  children=children,
121
145
  )
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: themefinder
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: A topic modelling Python package designed for analysing one-to-many question-answer data eg free-text survey responses.
5
5
  License: MIT
6
+ License-File: LICENCE
6
7
  Author: i.AI
7
8
  Author-email: packages@cabinetoffice.gov.uk
8
9
  Requires-Python: >=3.10,<3.13
@@ -17,7 +18,7 @@ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
18
  Classifier: Topic :: Text Processing :: Linguistic
18
19
  Requires-Dist: boto3 (>=1.29,<2.0)
19
20
  Requires-Dist: langchain
20
- Requires-Dist: langchain-openai (==0.1.17)
21
+ Requires-Dist: langchain-openai
21
22
  Requires-Dist: langfuse (==2.29.1)
22
23
  Requires-Dist: openpyxl (>=3.1.5,<4.0.0)
23
24
  Requires-Dist: pandas (>=2.2.2,<3.0.0)
@@ -0,0 +1,19 @@
1
+ themefinder/__init__.py,sha256=k3D3TpAvRdcXXZbHc_Lb7DsB53JwoGA0S4Ap5iX7PEw,477
2
+ themefinder/core.py,sha256=k-kkfWLz35Z2vyQwp50Cz6rVELq2eRWcHfRKNZGaaQ8,27186
3
+ themefinder/llm_batch_processor.py,sha256=Z9jm9Kr-6GD8g8kLkgdW97onjUbLLQ2M1YKwok39Q6Y,17652
4
+ themefinder/models.py,sha256=ZYtx1vvTcrFjhNG6yTej5nnwUcLnWakpS8GW7CmvbJM,13723
5
+ themefinder/prompts/agentic_theme_clustering.txt,sha256=FuvHD4jjCDBQ1ptTKYg0W9Bpsbwy7VeK1l-NzRoEmNM,2155
6
+ themefinder/prompts/consultation_system_prompt.txt,sha256=_A07oY_an4hnRx-9pQ0y-TLXJz0dd8vDI-MZne7Mdb4,89
7
+ themefinder/prompts/detail_detection.txt,sha256=hMB8yQR5y855TJLYSW3CNZDkLTPaA2lf9UJwH_GpkD4,1515
8
+ themefinder/prompts/sentiment_analysis.txt,sha256=vYCDhtEsG5I9xixwVhZbvKPJGU1Gqpw4-xAqGz72xhU,1671
9
+ themefinder/prompts/theme_condensation.txt,sha256=jqWKuPaSKrRGeYwNWTlVx45hfyWWhX1CvnKXrIiXxa0,1714
10
+ themefinder/prompts/theme_generation.txt,sha256=QRKW7DtcMSb2olT6j5jmdEPcXPMeZgogM-NYddEIKRk,1871
11
+ themefinder/prompts/theme_mapping.txt,sha256=0z6ddfYxRn1Ew4W3Su-16qTbWn2C6J2LMnK7Biu1tno,1621
12
+ themefinder/prompts/theme_refinement.txt,sha256=JDSYs2sdXqN-Yw9OWjfbmsl9x4Bn1J3oNVSsb_PQ5Ik,2433
13
+ themefinder/prompts/theme_target_alignment.txt,sha256=g7AVZLiP_xIH010X5SIZyG3q7gA6OBAplPv3xvmstOY,855
14
+ themefinder/theme_clustering_agent.py,sha256=L_IOjbfy41V7UE5Vd935KaCMlVOTCpHP2gF1Yl2trY0,13702
15
+ themefinder/themefinder_logging.py,sha256=n5SUQovEZLC4skEbxicjz_fOGF9mOk3S-Wpj5uXsaL8,314
16
+ themefinder-0.7.2.dist-info/METADATA,sha256=dK_IoZkCpuBZqZJYrPIy_x2HfATWCf0dmNh7bX6DOuQ,6748
17
+ themefinder-0.7.2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
18
+ themefinder-0.7.2.dist-info/licenses/LICENCE,sha256=C9ULIN0ctF60ZxUWH_hw1H434bDLg49Z-Qzn6BUHgqs,1060
19
+ themefinder-0.7.2.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.2.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,19 +0,0 @@
1
- themefinder/__init__.py,sha256=k3D3TpAvRdcXXZbHc_Lb7DsB53JwoGA0S4Ap5iX7PEw,477
2
- themefinder/core.py,sha256=mqToJ-ggx8JyholNMUwFDcAT35dWX8Hnt3BJzdaNgS0,26219
3
- themefinder/llm_batch_processor.py,sha256=Z9jm9Kr-6GD8g8kLkgdW97onjUbLLQ2M1YKwok39Q6Y,17652
4
- themefinder/models.py,sha256=JopmD4F23Mteh60m6WDpsuTs58dRc0tUbVX-d-L8Gv8,14680
5
- themefinder/prompts/agentic_theme_clustering.txt,sha256=6bHLpgZUQEaZXpLUB7EcMEbtXGqQ_1yniqZ6ZBJHFn0,1917
6
- themefinder/prompts/consultation_system_prompt.txt,sha256=_A07oY_an4hnRx-9pQ0y-TLXJz0dd8vDI-MZne7Mdb4,89
7
- themefinder/prompts/detail_detection.txt,sha256=6Vr_oN7rF5BCFipnCIHTSF8MmjerGyCixRWRT3vni1U,941
8
- themefinder/prompts/sentiment_analysis.txt,sha256=vYCDhtEsG5I9xixwVhZbvKPJGU1Gqpw4-xAqGz72xhU,1671
9
- themefinder/prompts/theme_condensation.txt,sha256=pHWuCtfU58gdtP2BfGZWOTvcb0MnTpb9OhOCGtkJv8U,1672
10
- themefinder/prompts/theme_generation.txt,sha256=QRKW7DtcMSb2olT6j5jmdEPcXPMeZgogM-NYddEIKRk,1871
11
- themefinder/prompts/theme_mapping.txt,sha256=HtGuStm-622TIEaqdb9LTaBs9xE-n9lvmcGQTG2_JOQ,2042
12
- themefinder/prompts/theme_refinement.txt,sha256=evWMCIEdeZCJ8zn4SBNgP6bmfAb0vzKiR5C5wfAjkUk,2649
13
- themefinder/prompts/theme_target_alignment.txt,sha256=g7AVZLiP_xIH010X5SIZyG3q7gA6OBAplPv3xvmstOY,855
14
- themefinder/theme_clustering_agent.py,sha256=Ie-5MFvIo7ukeeDXNpLawJXqLqBb6kvUGgSH6uTGL20,12826
15
- themefinder/themefinder_logging.py,sha256=n5SUQovEZLC4skEbxicjz_fOGF9mOk3S-Wpj5uXsaL8,314
16
- themefinder-0.7.0.dist-info/LICENCE,sha256=C9ULIN0ctF60ZxUWH_hw1H434bDLg49Z-Qzn6BUHgqs,1060
17
- themefinder-0.7.0.dist-info/METADATA,sha256=-PRjz0RTxp-yJsuavj8tw5NwtC1amsw12JyKNOitxZw,6737
18
- themefinder-0.7.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
19
- themefinder-0.7.0.dist-info/RECORD,,