finalfinal 1.0.0__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.
finalfinal/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ from .api import IncrementType, _parse_args, increment, reset, to_pdf, track
2
+
3
+ __all__ = ["track", "increment", "reset", "to_pdf", "IncrementType"]
4
+
5
+
6
+ def main() -> None:
7
+ _parse_args()
finalfinal/api.py ADDED
@@ -0,0 +1,1006 @@
1
+ """FinalFinal™ — The Enterprise-Grade™ File Versioning Solution Nobody Asked For.
2
+
3
+ This module provides a robust, scalable, and completely unnecessary file naming
4
+ management system. It solves the age-old problem of "report_final_FINAL_v2_ok_def.docx"
5
+ by replacing it with "report_final_FINAL_v2_ok_def_this-one_validated_100%_last.docx".
6
+
7
+ Progress.
8
+
9
+ Example:
10
+ Basic usage for the uninitiated::
11
+
12
+ from finalfinal import track, increment, reset, to_pdf
13
+
14
+ # Begin the cycle of suffering
15
+ path = track("my_presentation.pptx")
16
+
17
+ # Work in progress (allegedly)
18
+ path = increment(path, increment_type=IncrementType.WIP)
19
+
20
+ # It's definitely done this time
21
+ path = increment(path, increment_type=IncrementType.FINAL, certainty_level=2)
22
+
23
+ # It was not done
24
+ path = increment(path, increment_type=IncrementType.RETAKE)
25
+
26
+ # Archive the evidence and start fresh
27
+ path = reset(path)
28
+
29
+ # Generate a PDF that explains nothing
30
+ to_pdf(path)
31
+ """
32
+
33
+ import argparse
34
+ import re
35
+ import shutil
36
+ from datetime import datetime
37
+ from enum import Enum
38
+ from pathlib import Path
39
+ from random import choice, random
40
+ from typing import Optional
41
+
42
+ from reportlab.lib.pagesizes import A4
43
+ from reportlab.lib.styles import getSampleStyleSheet
44
+ from reportlab.lib.units import cm
45
+ from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer
46
+
47
+
48
+ class WhatAreYouTringToDoError(Exception):
49
+ """Raised when the user attempts something FinalFinal™ finds philosophically objectionable."""
50
+
51
+
52
+ TRACKING_SUFFIXES: list[str] = [
53
+ "to do",
54
+ "waiting",
55
+ "start",
56
+ "v1",
57
+ "fresh",
58
+ "v0",
59
+ "init",
60
+ "new",
61
+ "brand new",
62
+ "initial draft",
63
+ "first iteration",
64
+ "kickoff",
65
+ "baseline",
66
+ "origin",
67
+ "alpha",
68
+ "ground zero",
69
+ "genesis",
70
+ "draft",
71
+ "raw",
72
+ "untouched",
73
+ "unscathed",
74
+ ]
75
+
76
+ FINAL_SUFFIXES: list[str] = [
77
+ "final",
78
+ "def",
79
+ "definitive",
80
+ "last one",
81
+ "last",
82
+ "ultimate",
83
+ "all done",
84
+ "this one",
85
+ "validated",
86
+ "client validated",
87
+ "validated by supervisor",
88
+ "for real",
89
+ "do not touch",
90
+ "shipped",
91
+ "delivered",
92
+ "signed off",
93
+ "stamped and approved",
94
+ "carved in stone",
95
+ "FINAL",
96
+ "the one",
97
+ "fin",
98
+ "use this",
99
+ "farewell",
100
+ "for posterity",
101
+ ]
102
+
103
+ WIP_SUFFIXES: list[str] = [
104
+ "doing stuff",
105
+ "wip",
106
+ "updated",
107
+ "work",
108
+ "working",
109
+ "in progress",
110
+ "ongoing",
111
+ "bis",
112
+ "iterating",
113
+ "touch-ups",
114
+ "tweaks",
115
+ "micro-adjustments",
116
+ "minor edits",
117
+ "polish",
118
+ "refined",
119
+ "chaos",
120
+ "mess",
121
+ "not ready",
122
+ "do not look",
123
+ "needs love",
124
+ "touched",
125
+ ]
126
+
127
+ RETAKE_SUFFIXES: list[str] = [
128
+ "retake",
129
+ "error",
130
+ "redo",
131
+ "ugh",
132
+ "sigh...",
133
+ "whoops",
134
+ "oopsie daisy",
135
+ "mistake",
136
+ "oversight",
137
+ "wrong",
138
+ "nope",
139
+ "nah",
140
+ "nevermind",
141
+ "client feedback",
142
+ "stakeholder input",
143
+ "direction change",
144
+ "scope creep",
145
+ "updated brief",
146
+ "revised brief",
147
+ "my fault",
148
+ "their fault",
149
+ "monday morning version",
150
+ "post-meeting",
151
+ "after the call",
152
+ "dont even ask",
153
+ "yikes",
154
+ "lol",
155
+ "lmao",
156
+ ]
157
+
158
+ FIX_SUFFIXES: list[str] = [
159
+ "done",
160
+ "ok",
161
+ "fixed",
162
+ "settled",
163
+ "resolved",
164
+ "patched",
165
+ "addressed",
166
+ "corrected",
167
+ "remediated",
168
+ "sorted",
169
+ "handled",
170
+ "closed",
171
+ "better",
172
+ "improved",
173
+ "cleaner",
174
+ "tighter",
175
+ "crisper",
176
+ "sharper",
177
+ "nicer",
178
+ "good now",
179
+ "actually good",
180
+ "decent",
181
+ "not terrible",
182
+ "passable",
183
+ "yolo",
184
+ ]
185
+
186
+ DONE_SUFFIXES: list[str] = [
187
+ "done",
188
+ "ok",
189
+ "ready",
190
+ "yes",
191
+ "approved",
192
+ "to deliver",
193
+ "for review",
194
+ "delivery",
195
+ "ready to go",
196
+ "good",
197
+ "decent",
198
+ "awaiting validation",
199
+ "ship it",
200
+ "for client",
201
+ "for producer",
202
+ "for boss",
203
+ "for stakeholders",
204
+ "pending approval",
205
+ "submitted",
206
+ "in review",
207
+ "not my problem anymore",
208
+ "their problem now",
209
+ "launching",
210
+ ]
211
+
212
+ WEAK_CERTITUDE_MARKERS: list[str] = [
213
+ "almost",
214
+ "nearly",
215
+ "not quite",
216
+ "about",
217
+ "just about",
218
+ "more or less",
219
+ "roughly",
220
+ "surely",
221
+ "pretty much",
222
+ "practically",
223
+ "virtually",
224
+ "next to",
225
+ "close to",
226
+ "not far from",
227
+ "nigh on",
228
+ "approximatively",
229
+ "pretty well",
230
+ "mostly",
231
+ "sort of",
232
+ "kind of",
233
+ "technically",
234
+ "loosely speaking",
235
+ "arguably",
236
+ "debatably",
237
+ "let's say",
238
+ "supposedly",
239
+ "allegedly",
240
+ "fingers crossed",
241
+ "theoretically",
242
+ ]
243
+
244
+ STRONG_CERTITUDE_MARKERS: list[str] = [
245
+ "sure",
246
+ "100%",
247
+ "definitely",
248
+ "certainly",
249
+ "undeniably",
250
+ "without doubt",
251
+ "undoublebly",
252
+ "absolutely",
253
+ "positively",
254
+ "guaranteed",
255
+ "beyond doubt",
256
+ ]
257
+
258
+ RESTART_SUFFIXES: list[str] = [
259
+ "redo",
260
+ "start over",
261
+ "restart",
262
+ "milestone",
263
+ "clean",
264
+ "new start",
265
+ "fresh start",
266
+ "new2",
267
+ "after backup",
268
+ "reboot",
269
+ "reset",
270
+ "take two",
271
+ "second attempt",
272
+ "third time lucky",
273
+ "this time for sure",
274
+ "clean slate",
275
+ "new leaf",
276
+ "phoenix",
277
+ "post-disaster",
278
+ "after the incident",
279
+ "lessons learned",
280
+ "with wisdom",
281
+ "humbled",
282
+ "enlightened",
283
+ "matured",
284
+ "square one",
285
+ "post-feedback",
286
+ "post-therapy",
287
+ "renewed",
288
+ "reborn",
289
+ "v2 from scratch",
290
+ "rebuild",
291
+ "refactor",
292
+ ]
293
+
294
+ CHANGELOG_SENTENCES: list[str] = [
295
+ "Someone did something.",
296
+ "The file was modified in some way.",
297
+ "God knows what happened on {dt}, but a new version of the file was saved.",
298
+ "At {dt}, an unspecified change was made by an unspecified individual for unspecified reasons.",
299
+ "A human being interacted with this file on {dt}. Motivation: unknown.",
300
+ "Something was altered. Details are scarce. This is intentional.",
301
+ "The file grew. No one knows by how much. No one asked.",
302
+ "Edits were performed. Whether they were improvements is a matter of perspective.",
303
+ "On {dt}, the file was opened, stared at, and then saved. Progress.",
304
+ "Version increment detected.",
305
+ "A spontaneous modification event occurred at approximately {dt}.",
306
+ "The file was touched.",
307
+ "Changes were made. At this stage, we choose to believe they were necessary.",
308
+ "At {dt}, someone clicked Save. We'll count that as work.",
309
+ "The file evolved.",
310
+ "This version exists because someone, somewhere, was not satisfied with the previous one.",
311
+ "A new version was born into this world at {dt}, screaming and unnamed.",
312
+ "The file was improved, allegedly, at {dt}.",
313
+ "An act of creation — or destruction — took place at {dt}. Records are unclear.",
314
+ "The binary gods smiled upon this file at {dt} and decreed: it shall be different.",
315
+ "Undocumented changes were made by an undocumented person in an undocumented manner.",
316
+ "At {dt}, a brave soul made a modification and did not write a commit message.",
317
+ "The contents of this file shifted, as does our confidence in the project overall.",
318
+ "Version history note: see other versions for context.",
319
+ ]
320
+
321
+ SEPARATORS: list[str] = ["-", "_", " "]
322
+ SUFFIX_SEPARATORS: list[str] = ["-", "_", " ", ""]
323
+
324
+
325
+ class IncrementType(str, Enum):
326
+ """The taxonomy of despair, formalized.
327
+
328
+ Each member represents a distinct emotional and professional state
329
+ in the lifecycle of a file that should probably be under git.
330
+
331
+ Attributes:
332
+ WIP: Work In Progress. Or Work In Paralysis. Tomato, tomato.
333
+ RETAKE: Something went wrong. Again. It's fine. It's fine.
334
+ FIX: A specific error has been addressed. Others remain. Unacknowledged.
335
+ DONE: The file is done. (It is not done.)
336
+ FINAL: The file is final. (See: DONE.)
337
+ """
338
+
339
+ WIP = "wip"
340
+ RETAKE = "retake"
341
+ FIX = "fix"
342
+ DONE = "done"
343
+ FINAL = "final"
344
+
345
+
346
+ _SUFFIX_MAP: dict[IncrementType, list[str]] = {
347
+ IncrementType.WIP: WIP_SUFFIXES,
348
+ IncrementType.RETAKE: RETAKE_SUFFIXES,
349
+ IncrementType.FIX: FIX_SUFFIXES,
350
+ IncrementType.DONE: DONE_SUFFIXES,
351
+ IncrementType.FINAL: FINAL_SUFFIXES,
352
+ }
353
+
354
+
355
+ def _upper(word: str) -> str:
356
+ """Transforms a word into SCREAMING CASE. For emphasis. Or panic."""
357
+ return word.upper()
358
+
359
+
360
+ def _lower(word: str) -> str:
361
+ """Transforms a word into Sentence case. Professional. Approachable. Forgettable."""
362
+ return word.capitalize()
363
+
364
+
365
+ def _title(word: str) -> str:
366
+ """Transforms A Word Into Title Case. Very Important. Very Serious."""
367
+ return word.title()
368
+
369
+
370
+ def _camel_case(word: str) -> str:
371
+ """Transforms a word intoCamelCase. For developers who got lost on their way to git."""
372
+ word = word.title()
373
+ return word[:1].lower() + word[1:]
374
+
375
+
376
+ _CASING_CALLBACKS = [_upper, _lower, _title, _camel_case]
377
+ """The four horsemen of inconsistent typography."""
378
+
379
+
380
+ def random_separator() -> str:
381
+ """Returns a randomly selected separator character.
382
+
383
+ Ensures that no two versions of the same filename look quite the same,
384
+ which is the entire point of this enterprise.
385
+
386
+ Returns:
387
+ A separator string from ``SEPARATORS`` (``"-"``, ``"_"``, or ``" "``).
388
+ """
389
+ return choice(SEPARATORS)
390
+
391
+
392
+ def randomize_casing(word: str) -> str:
393
+ """Applies a randomly selected casing transformation to a word.
394
+
395
+ The transformation is selected from upper, capitalize, title, and camelCase.
396
+ The result is unpredictable. This is a feature, not a bug.
397
+
398
+ Args:
399
+ word: The input string, in whatever casing it currently suffers under.
400
+
401
+ Returns:
402
+ The word, in a new and unexpected casing. Yay.
403
+ """
404
+ return choice(_CASING_CALLBACKS)(word)
405
+
406
+
407
+ def randomize_separators(word: str) -> str:
408
+ """Replaces spaces in a word with a randomly chosen separator (or nothing).
409
+
410
+ For multi-word suffixes like "brand new" or "oopsie daisy", this function
411
+ ensures they emerge with maximum visual clarity.
412
+
413
+ Args:
414
+ word: A potentially multi-word string containing spaces.
415
+
416
+ Returns:
417
+ The word with spaces replaced by a random entry from ``SUFFIX_SEPARATORS``.
418
+ """
419
+ return word.replace(" ", choice(SUFFIX_SEPARATORS))
420
+
421
+
422
+ def random_word(word_list: list[str]) -> str:
423
+ """Picks a random word from a list and applies randomized casing and separators.
424
+
425
+ The randomization is non-negotiable. Do not attempt to predict it.
426
+ Do not attempt to control it. Simply accept the output and move on.
427
+
428
+ Args:
429
+ word_list: A non-empty list of candidate suffix strings.
430
+
431
+ Returns:
432
+ A randomly selected, case-mangled, separator-scrambled suffix.
433
+ """
434
+ word = choice(word_list)
435
+ word = randomize_casing(word)
436
+ word = randomize_separators(word)
437
+ return word
438
+
439
+
440
+ def _normalize(text: str) -> str:
441
+ """Normalizes a string for deduplication comparison.
442
+
443
+ Strips all separators and converts to lowercase, because the file system
444
+ doesn't care about your aesthetic choices and neither does this function.
445
+
446
+ Args:
447
+ text: A string. Duh.
448
+
449
+ Returns:
450
+ A lowercase, separator-free version of the input.
451
+ """
452
+ return re.sub(r"[-_ ]", "", text).lower()
453
+
454
+
455
+ def _suffix_already_present(path: Path, candidate: str) -> bool:
456
+ """Checks whether a candidate suffix is already embedded in the filename.
457
+
458
+ Performs a normalized, case-insensitive, separator-agnostic comparison,
459
+ because the previous version of the file was called
460
+ ``report-wip_WIP-Wip.docx`` and no one is laughing.
461
+
462
+ Args:
463
+ path: The ``Path`` object representing the current file.
464
+ candidate: The proposed suffix to add, pre-separator but post-word.
465
+
466
+ Returns:
467
+ ``True`` if the suffix is already present (normalized); ``False`` otherwise.
468
+ """
469
+ normalized_stem = _normalize(path.stem)
470
+ normalized_candidate = _normalize(candidate)
471
+ return normalized_candidate in normalized_stem
472
+
473
+
474
+ def _pick_unique_suffix(word_list: list[str], path: Path) -> str:
475
+ """Picks a suffix that does not already appear in the filename.
476
+
477
+ Shuffles through the word list until it finds one that isn't already
478
+ baked into the path. If all suffixes are already present — congratulations,
479
+ your filename is longer than most legal documents.
480
+
481
+ Args:
482
+ word_list: The pool of candidate suffix strings to draw from.
483
+ path: The current ``Path``, used for duplicate detection.
484
+
485
+ Returns:
486
+ A suffix string that is (probably) not already in the filename.
487
+
488
+ Raises:
489
+ WhatAreYouTringToDoError: If every single suffix in the list has
490
+ already been used. You have achieved something. We're not sure what.
491
+ """
492
+ shuffled = list(word_list)
493
+ tried: list[str] = []
494
+ for _ in range(
495
+ len(shuffled) * 3
496
+ ): # generous attempts, because hope springs eternal
497
+ candidate_raw = choice(shuffled)
498
+ candidate_cased = randomize_casing(candidate_raw)
499
+ candidate_sep = randomize_separators(candidate_cased)
500
+ if not _suffix_already_present(path, candidate_raw):
501
+ return candidate_sep
502
+ tried.append(candidate_raw)
503
+
504
+ raise WhatAreYouTringToDoError(
505
+ f"Every suffix in this category already appears in '{path.name}'. "
506
+ "Your filename is a monument to indecision. Consider starting over. "
507
+ "FinalFinal™ has a reset() function for exactly this kind of situation."
508
+ )
509
+
510
+
511
+ def get_metadata_file(path: Path | str) -> Path:
512
+ """Returns the path to the FinalFinal™ metadata file for a given tracked file.
513
+
514
+ The metadata file is a sacred artifact named ``important_notes_DONT_DELETE.docx``
515
+ (it is not a real docx), stored in the same directory as the tracked file.
516
+ Deleting it is the leading cause of WhatAreYouTringToDoError in the workplace.
517
+
518
+ Args:
519
+ path: The ``Path`` (or path-like string) of the tracked file.
520
+
521
+ Returns:
522
+ The ``Path`` to the corresponding metadata file.
523
+ """
524
+ path = Path(path)
525
+ return path.with_name("important_notes_DONT_DELETE.docx")
526
+
527
+
528
+ def _get_suffix_string(separator: str, word: str) -> str:
529
+ """Concatenates a separator and a word into a suffix fragment.
530
+
531
+ If the separator is empty, the word is capitalized for visual distinction.
532
+ This is the one place in FinalFinal™ where something intentional happens.
533
+
534
+ Args:
535
+ separator: A separator character from ``SEPARATORS``.
536
+ word: The already-cased, already-separated suffix word.
537
+
538
+ Returns:
539
+ The combined separator + word string ready for appending.
540
+ """
541
+ if not separator:
542
+ return word.capitalize()
543
+ return separator + word
544
+
545
+
546
+ def add_suffix(path: Path, word_list: list[str]) -> Path:
547
+ """Appends a randomly chosen, deduplicated suffix to a file path.
548
+
549
+ Combines a random separator with a word drawn from ``word_list``,
550
+ verified not to already appear in the filename.
551
+
552
+ Args:
553
+ path: The ``Path`` to which the suffix should be appended.
554
+ word_list: The pool of candidate suffix strings.
555
+
556
+ Returns:
557
+ A new ``Path`` with the suffix appended before the file extension.
558
+ """
559
+ separator = random_separator()
560
+ word = _pick_unique_suffix(word_list, path)
561
+ suffix_str = _get_suffix_string(separator, word)
562
+ name = path.stem + suffix_str + path.suffix
563
+ return path.with_name(name)
564
+
565
+
566
+ def is_tracked(path: Path) -> bool:
567
+ """Checks whether a file is currently being tracked by FinalFinal™.
568
+
569
+ Detection is performed by checking for the presence of the metadata file.
570
+ If the metadata file has been deleted, FinalFinal™ will pretend the file
571
+ never existed. This is called "deniability."
572
+ Do not ask for a more robust system: we already wrote DONT_DELETE in upper
573
+ case, YOU are responsible if the versioning framework breaks.
574
+
575
+ Args:
576
+ path: The ``Path`` to check for tracked status.
577
+
578
+ Returns:
579
+ ``True`` if the metadata file exists; ``False`` if it was deleted
580
+ (or never existed).
581
+ """
582
+ return get_metadata_file(path).exists()
583
+
584
+
585
+ def track(path: Path | str) -> Path:
586
+ """Begins tracking a file with FinalFinal™. Godspeed.
587
+
588
+ Creates a copy of the file with a randomized tracking suffix and records
589
+ the original filename in the metadata file. The original file is left
590
+ untouched, because FinalFinal™ respects boundaries.
591
+
592
+ Args:
593
+ path: The ``Path`` (or path-like string) of the file to track.
594
+ Must exist on disk. FinalFinal™ cannot track hypothetical files.
595
+
596
+ Returns:
597
+ The ``Path`` to the newly created, suffix-adorned copy of the file.
598
+
599
+ Raises:
600
+ WhatAreYouTringToDoError: If the file does not exist, or if it is
601
+ already being tracked. You had one job.
602
+ """
603
+ path = Path(path).resolve()
604
+ if not path.exists():
605
+ raise WhatAreYouTringToDoError(
606
+ "The file does not exist. How could FinalFinal™ possibly track it?"
607
+ )
608
+
609
+ original_path_name = path.name
610
+ metadata_path = get_metadata_file(path)
611
+ if is_tracked(path):
612
+ raise WhatAreYouTringToDoError(
613
+ f"File {path.name} is already tracked by FinalFinal™"
614
+ )
615
+
616
+ new_path = add_suffix(path, TRACKING_SUFFIXES)
617
+ shutil.copy(path, new_path)
618
+
619
+ if not metadata_path.exists():
620
+ metadata_path.write_text(original_path_name)
621
+
622
+ return new_path
623
+
624
+
625
+ def get_latest(path: Path) -> Path:
626
+ """Retrieves the most recent version of a tracked file.
627
+
628
+ "Most recent" is defined as the file whose name is the longest,
629
+ because each iteration appends a suffix, and length is the closest
630
+ thing to version history this system has.
631
+
632
+ Args:
633
+ path: Any ``Path`` in the tracked file's directory. It need not be
634
+ the latest version; FinalFinal™ will find it regardless.
635
+
636
+ Returns:
637
+ The ``Path`` to the longest-named file matching the original stem
638
+ and extension. Presumably the most recent. Hopefully.
639
+
640
+ Raises:
641
+ WhatAreYouTringToDoError: If the metadata file is missing
642
+ (see: ``is_tracked``), or if no matching files can be found
643
+ """
644
+ metadata_path = get_metadata_file(path)
645
+ if not metadata_path.exists():
646
+ raise WhatAreYouTringToDoError(
647
+ f"The file {path.name} is not tracked by FinalFinal™"
648
+ )
649
+ original_file = Path(metadata_path.read_text())
650
+
651
+ files = [
652
+ file
653
+ for file in path.parent.iterdir()
654
+ if file.is_file()
655
+ and file.name.startswith(original_file.stem)
656
+ and file.suffix == original_file.suffix
657
+ ]
658
+ files.sort(key=lambda x: len(x.name))
659
+ if not files:
660
+ raise WhatAreYouTringToDoError(
661
+ f'No file with prefix "{original_file.stem}" could not be found'
662
+ )
663
+ return files[-1]
664
+
665
+
666
+ def get_incremented_path(
667
+ path: Path | str,
668
+ increment_type: IncrementType = IncrementType.WIP,
669
+ custom_suffix: Optional[str] = None,
670
+ certainty_level: int = 1,
671
+ ) -> Path:
672
+ """Computes the new path a file would receive upon incrementation, without moving anything.
673
+
674
+ This is the purely theoretical arm of the operation. It calculates destiny
675
+ without committing to it — much like your project manager during sprint planning.
676
+
677
+ The suffix pool is selected by ``increment_type``, then optionally prefixed
678
+ with a certitude marker depending on ``certainty_level``:
679
+
680
+ - ``certainty_level < 1``: Weak certitude (e.g., "roughly final", "sort of done")
681
+ - ``certainty_level == 1``: No certitude marker (blissful neutrality)
682
+ - ``certainty_level > 1``: Strong certitude (e.g., "100% final", "bank on it done")
683
+
684
+ For ``IncrementType.WIP``, there is additionally a 37.569% chance the last digit
685
+ of the stem is incremented instead of appending a word suffix. We believe it is
686
+ smarter than just naming your file "wip-wip-wip".
687
+
688
+ Args:
689
+ path: The ``Path`` (or path-like string) from which to compute the new path.
690
+ The latest version will be located automatically.
691
+ increment_type: The type of increment to perform. Defaults to ``IncrementType.WIP``
692
+ because statistically, that is where you are.
693
+ custom_suffix: An optional override suffix. If provided, all certitude and
694
+ type logic is bypassed. You get exactly what you asked for. You probably
695
+ shouldn't have asked for it.
696
+ certainty_level: An integer expressing how sure you are. Values below 1
697
+ express doubt. Values above 1 express overconfidence. Both are delusional
698
+ in their own way. Defaults to ``1`` (affecting neutrality).
699
+
700
+ Returns:
701
+ A ``Path`` representing what the file would be renamed to.
702
+ Nothing on disk is changed. This is merely a premonition.
703
+ """
704
+ path = Path(path)
705
+ path = get_latest(path)
706
+
707
+ if custom_suffix:
708
+ return add_suffix(path, [custom_suffix])
709
+
710
+ word_list = list(_SUFFIX_MAP[increment_type])
711
+
712
+ if certainty_level < 1:
713
+ certainty_word = choice(WEAK_CERTITUDE_MARKERS)
714
+ word_list = [f"{certainty_word} {w}" for w in word_list]
715
+ elif certainty_level > 1:
716
+ certainty_word = choice(STRONG_CERTITUDE_MARKERS)
717
+ word_list = [f"{certainty_word} {w}" for w in word_list]
718
+
719
+ if increment_type == IncrementType.WIP and random() < 0.37569:
720
+ stem = path.stem
721
+ if stem[-1].isdigit() and certainty_level == 1:
722
+ path = path.with_stem(stem[:-1] + str(int(stem[-1]) + 1))
723
+ else:
724
+ path = path.with_stem(stem + "2")
725
+ return path
726
+
727
+ return add_suffix(path, word_list)
728
+
729
+
730
+ def increment(
731
+ path: Path | str,
732
+ increment_type: IncrementType = IncrementType.WIP,
733
+ certainty_level: int = 1,
734
+ custom_suffix: Optional[str] = None,
735
+ overwrite: bool = True,
736
+ ) -> Path:
737
+ """Renames (or copies) the latest version of a tracked file with a new suffix.
738
+
739
+ This is the primary action of FinalFinal™. Each call represents one step
740
+ further into the filename. Each step is, statistically, not the last.
741
+
742
+ Args:
743
+ path: The ``Path`` (or path-like string) to any version of the tracked file.
744
+ The latest version will be located and operated upon.
745
+ increment_type: The emotional register of the new suffix. Defaults to
746
+ ``IncrementType.WIP``, which is where we all are, always.
747
+ certainty_level: How confident you are that this version matters.
748
+ Accepts any integer. Only three ranges are meaningful:
749
+ below 1 (uncertain), 1 (neutral), above 1 (dangerously confident).
750
+ custom_suffix: An escape hatch for those who wish to bypass the carefully
751
+ curated suffix pools and write their own destiny. Literally.
752
+ overwrite: If ``True`` (default), the current latest file is renamed.
753
+ If ``False``, a copy is made and the original is preserved.
754
+ Set to ``False`` if you are the kind of person who keeps both.
755
+ You know who you are.
756
+
757
+ Returns:
758
+ The ``Path`` of the newly named (or newly created) file version.
759
+ """
760
+ path = Path(path)
761
+ path = get_latest(path)
762
+ new_path = get_incremented_path(
763
+ path,
764
+ increment_type=increment_type,
765
+ certainty_level=certainty_level,
766
+ custom_suffix=custom_suffix,
767
+ )
768
+ if overwrite:
769
+ path.rename(new_path)
770
+ else:
771
+ shutil.copy(path, new_path)
772
+ return new_path
773
+
774
+
775
+ def reset(path: Path | str, backup_folder_name: str = "OLD") -> Path:
776
+ """Archives all versioned files into a backup folder and starts fresh.
777
+
778
+ When the filename has grown to a length that violates several international
779
+ conventions, it is time to reset. This function moves all files matching
780
+ the original stem into a subfolder (``OLD`` by default, or whatever you'd
781
+ like to call the archive of your failures), then creates a new clean file
782
+ using one of the RESTART_SUFFIXES.
783
+
784
+ The metadata file is preserved. It has seen too much to be discarded.
785
+
786
+ Args:
787
+ path: The ``Path`` (or path-like string) of any version of the tracked file.
788
+ All matching versions will be located and relocated.
789
+ backup_folder_name: The name of the subfolder into which the old versions
790
+ will be moved. Defaults to ``"OLD"``. ``"BEFORE_THE_INCIDENT"`` is
791
+ also acceptable, and encouraged.
792
+
793
+ Returns:
794
+ The ``Path`` of the newly created clean file with a fresh restart suffix.
795
+
796
+ Raises:
797
+ WhatAreYouTringToDoError: If the file is not tracked (how did you get here?),
798
+ or if no matching files are found (where did they go?).
799
+ """
800
+ path = Path(path).resolve()
801
+ metadata_path = get_metadata_file(path)
802
+
803
+ if not metadata_path.exists():
804
+ raise WhatAreYouTringToDoError(
805
+ f"The file {path.name} is not tracked by FinalFinal™. "
806
+ "FinalFinal™ cannot reset what it never held."
807
+ )
808
+
809
+ original_file = Path(metadata_path.read_text())
810
+ parent = path.parent
811
+
812
+ # Collect all version files (not the metadata)
813
+ version_files = [
814
+ f
815
+ for f in parent.iterdir()
816
+ if f.is_file()
817
+ and f.name.startswith(original_file.stem)
818
+ and f.suffix == original_file.suffix
819
+ and f != metadata_path
820
+ ]
821
+
822
+ if not version_files:
823
+ raise WhatAreYouTringToDoError(
824
+ f'No files with prefix "{original_file.stem}" were found. '
825
+ "Perhaps they were never real. Perhaps you were never real."
826
+ )
827
+
828
+ # Create backup folder
829
+ backup_dir = parent / backup_folder_name
830
+ backup_dir.mkdir(exist_ok=True)
831
+
832
+ # Move all old versions into backup
833
+ for f in version_files:
834
+ shutil.move(str(f), backup_dir / f.name)
835
+
836
+ # Create a fresh file with a restart suffix
837
+ latest_backup = sorted(
838
+ [backup_dir / f.name for f in version_files], key=lambda x: len(x.name)
839
+ )[-1]
840
+ new_path = add_suffix(parent / original_file.name, RESTART_SUFFIXES)
841
+ shutil.copy(backup_dir / latest_backup.name, new_path)
842
+
843
+ return new_path
844
+
845
+
846
+ def prune(path: Path) -> Path:
847
+ """Removes old, backup, and clearly redundant versions of a tracked file.
848
+
849
+ This function is a placeholder: as it will permanently damage files on your
850
+ computer and servers, we take extra care to make it as safe as possible.
851
+ It will be implemented it the final version of the python package.
852
+
853
+ Args:
854
+ path: The ``Path`` to the tracked file whose older versions should be pruned.
855
+
856
+ Returns:
857
+ The ``Path`` to the surviving file. Presumably.
858
+ """
859
+ ...
860
+
861
+
862
+ def to_pdf(path: Path | str, pdf_path: Path | str | None = None) -> Path:
863
+ """Generates a PDF changelog from all versions of a tracked file.
864
+
865
+ Iterates over all files matching the original tracked stem, sorted by
866
+ filename length (a reasonable proxy for chronological order, given that
867
+ this system does not use timestamps, git, or common sense). For each file,
868
+ retrieves the filesystem modification time and pairs it with a randomly
869
+ selected changelog sentence, because actual commit messages were not
870
+ provided. Commit Messages were an actual feature of FinalFinal™, in its
871
+ initial version, but we decided to drop it, because typing a few words
872
+ made the user experience too clunky.
873
+
874
+ The resulting PDF is named after the original file with a ``_changelog``
875
+ suffix, and is saved in the same directory. It may be emailed to a producer,
876
+ who may or may not read it. This is out of the scope of FinalFinal™.
877
+
878
+ Args:
879
+ path: The ``Path`` (or path-like string) of any version of the tracked file.
880
+ pdf_path: The ``Path`` (or path-like string) to the output pdf file.
881
+
882
+ Returns:
883
+ The ``Path`` to the generated PDF changelog.
884
+
885
+ Raises:
886
+ WhatAreYouTringToDoError: If the file is not tracked, or if no matching
887
+ versions can be found.
888
+ """
889
+ path = Path(path).resolve()
890
+ pdf_path = Path(pdf_path).resolve() if pdf_path else None
891
+ metadata_path = get_metadata_file(path)
892
+
893
+ if not metadata_path.exists():
894
+ raise WhatAreYouTringToDoError(
895
+ f"The file {path.name} is not tracked by FinalFinal™. "
896
+ "There is no history to document. There is no history at all, really."
897
+ )
898
+
899
+ original_file = Path(metadata_path.read_text())
900
+ parent = path.parent
901
+
902
+ # Gather all version files and sort by name length (our proxy for chronology)
903
+ version_files = sorted(
904
+ [
905
+ f
906
+ for f in parent.iterdir()
907
+ if f.is_file()
908
+ and f.name.startswith(original_file.stem)
909
+ and f.suffix == original_file.suffix
910
+ ],
911
+ key=lambda x: len(x.name),
912
+ )
913
+
914
+ if not version_files:
915
+ raise WhatAreYouTringToDoError(
916
+ "No version files were found. The history is empty. "
917
+ "Much like this project's documentation."
918
+ )
919
+
920
+ # Build changelog entries
921
+ entries: list[tuple[str, str]] = []
922
+ for f in version_files:
923
+ mtime = datetime.fromtimestamp(f.stat().st_mtime)
924
+ dt_str = mtime.strftime("%Y-%m-%d at %H:%M:%S")
925
+ sentence_template = choice(CHANGELOG_SENTENCES)
926
+ sentence = sentence_template.format(dt=dt_str)
927
+ entries.append((f.name, sentence))
928
+
929
+ # Generate PDF
930
+ if not pdf_path:
931
+ pdf_path = parent / f"{original_file.stem}_changelog.pdf"
932
+ doc = SimpleDocTemplate(
933
+ str(pdf_path),
934
+ pagesize=A4,
935
+ leftMargin=2 * cm,
936
+ rightMargin=2 * cm,
937
+ topMargin=2.5 * cm,
938
+ bottomMargin=2 * cm,
939
+ )
940
+ styles = getSampleStyleSheet()
941
+ story = []
942
+
943
+ # Title
944
+ story.append(Paragraph("FinalFinal&#8482; Changelog", styles["Title"]))
945
+ story.append(Paragraph(f"File: {original_file.name}", styles["Heading2"]))
946
+ story.append(
947
+ Paragraph(
948
+ f"Generated: {datetime.now().strftime('%Y-%m-%d at %H:%M:%S')}",
949
+ styles["Normal"],
950
+ )
951
+ )
952
+ story.append(Spacer(1, 0.5 * cm))
953
+ story.append(
954
+ Paragraph(
955
+ "The following represents the complete audit trail of this file. "
956
+ "It should not be used in a court of law.",
957
+ styles["Italic"],
958
+ )
959
+ )
960
+ story.append(Spacer(1, 0.8 * cm))
961
+
962
+ # Entries
963
+ for i, (filename, sentence) in enumerate(entries, start=1):
964
+ story.append(Paragraph(f"<b>Version {i}:</b> {filename}", styles["Heading3"]))
965
+ story.append(Paragraph(sentence, styles["Normal"]))
966
+ story.append(Spacer(1, 0.4 * cm))
967
+
968
+ doc.build(story)
969
+ return pdf_path
970
+
971
+
972
+ def _parse_args():
973
+ parser = argparse.ArgumentParser()
974
+ parser.add_argument("-p", "--path", required=True, type=Path)
975
+ action_group = parser.add_mutually_exclusive_group()
976
+ action_group.add_argument("-t", "--track", action="store_true")
977
+ action_group.add_argument("-i", "--increment", action="store_true")
978
+ action_group.add_argument("-pdf", "--export_pdf", action="store_true")
979
+ action_group.add_argument("-r", "--reset", action="store_true")
980
+ parser.add_argument("-pf", "--pdf_file", required=False, type=Path)
981
+ parser.add_argument(
982
+ "-it", "--increment_type", type=IncrementType, default=IncrementType.WIP
983
+ )
984
+ parser.add_argument("-cl", "--certainty_level", required=False, type=int, default=1)
985
+ parser.add_argument("-cs", "--custom_suffix", required=False, type=str)
986
+ parser.add_argument("-ow", "--overwrite", action="store_true")
987
+ args = parser.parse_args()
988
+
989
+ if args.track:
990
+ track(args.path)
991
+ if args.increment:
992
+ increment(
993
+ args.path,
994
+ increment_type=args.increment_type,
995
+ certainty_level=args.certainty_level,
996
+ overwrite=args.overwrite,
997
+ custom_suffix=args.custom_suffix,
998
+ )
999
+ if args.export_pdf:
1000
+ to_pdf(args.path, args.pdf_file)
1001
+ if args.reset:
1002
+ reset(path=args.path)
1003
+
1004
+
1005
+ if __name__ == "__main__":
1006
+ _parse_args()
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: finalfinal
3
+ Version: 1.0.0
4
+ Summary: FinalFinal™ is a file versioning system that encodes the project history directly into the filename, where it obviously belongs.
5
+ Keywords: filesystem,version control
6
+ Author: tlanguebien
7
+ Author-email: tlanguebien <tlanguebien@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Requires-Dist: reportlab
11
+ Requires-Python: >=3.9
12
+ Project-URL: Homepage, https://github.com/tristanlanguebien/finalfinal
13
+ Description-Content-Type: text/markdown
14
+
15
+ # FinalFinal™
16
+ ### *Where Done Is Just Another Iteration.*
17
+
18
+ **FinalFinal™** is a file versioning system that encodes the project history directly into the filename, where it obviously belongs.
19
+ No Git, no SVN: **The filename is the changelog.**
20
+
21
+ ## Use Case
22
+
23
+ In the age of deliverable-oriented agility, versioning alone is no longer enough. You need to *tell a story*. With FinalFinal™, every file becomes a compressed roadmap, a monument to the iterative process:
24
+
25
+ ```
26
+ myfile_NEW_forProducers_minor-editsV2_FINAL-MostlyApproved.pdf
27
+ ```
28
+
29
+ Each suffix is a chapter:
30
+
31
+ | Suffix | What it communicates |
32
+ |---|---|
33
+ | `NEW` | Ambition and optimism |
34
+ | `forProducers` | Illusion of governance |
35
+ | `minor-editsV2` | Resilience in the face of client feedback |
36
+ | `FINAL` | The conclusion of a production cycle (Provisional) |
37
+ | `MostlyApproved` | Aknowledges endless possibilities |
38
+
39
+ Every additional suffix reinforces collective confidence. If you have reached `final-v3-def-ok2`, you know the project is moving forward.
40
+
41
+ ## Features
42
+
43
+ ### Context-Driven Incrementation
44
+
45
+ `wip`, `retake`, `fix`, `done`, and `final` are handled as distinct increment types, each drawing from a curated pool of suffixes. FinalFinal™ helps you write the perfect narrative for your file, one version at a time.
46
+
47
+ ### Certainty Levels
48
+
49
+ Are you unsure about your changes? Quietly confident? Dangerously overcommitted? FinalFinal™'s collection of carefully engineered affixes lets you compose version names with genuine emotional nuance:
50
+
51
+ - Low certainty: `report_probably-fixed.docx`, `brief sortOfDone.pdf`
52
+ - High certainty: `contract_100%_DEFINITIVE.docx`
53
+
54
+ ### Reset
55
+
56
+ At some point, your filename will be ungodly long. This is not a flaw, just a sign that the project has lived.
57
+
58
+ The `reset` feature archives everything into a tidy `_OLD` folder (or `BEFORE_THE_INCIDENT`, if you prefer) and starts you fresh with one of our curated restart suffixes. You may then begin making the same mistakes again.
59
+
60
+ ### PDF Export
61
+
62
+ At some point, someone will ask for a changelog. `to_pdf` generates a professional PDF documenting every version of your file, sorted by filename length (the closest available proxy for chronological order), annotated with modification descriptions such as:
63
+
64
+ > *"At 2024-11-04 at 14:32:17, someone clicked Save. We'll count that as work."*
65
+
66
+ Send it by email. Your inbox becomes your audit trail. This is fine.
67
+
68
+ ## Technical Architecture
69
+
70
+ FinalFinal™ is powered by our proprietary **Recursive Semantic Drift™** engine, which enables:
71
+
72
+ - **Unlimited suffix stacking**: no enforced ceiling = endless possibilities.
73
+ - **Emotional and certitude encoding**: affixes such as `maybe`, `definitely`, `god-knows`, and `not-my-problem-anymore` allow for nuanced sentiment to be embedded directly in the file path.
74
+ - **Multiple coexisting final versions**: because sometimes the client validates two things on the same afternoon.
75
+
76
+ ### Compatibility
77
+
78
+ FinalFinal™ is fully compatible with all modern file distribution infrastructure:
79
+
80
+ - 📧 Email (recommended)
81
+ - 💾 USB drives
82
+ - ☁️ Google Drive
83
+ - 🗂️ Network Shares named `\\PROD-FINAL\FINAL`
84
+
85
+ ## Security & Compliance
86
+
87
+ FinalFinal™ is compliant with the following standards:
88
+
89
+ - **ISO-Good-Enough** (validated by nobody in particular)
90
+ - **Internally Ratified in a Meeting** (see the invite calendar for proof)
91
+ - **I.W.O.M.C.** (It Works On My Computer, the gold standard of pre-delivery testing)
92
+
93
+ > [!CAUTION]
94
+ > FinalFinal™ does not implement access control, version locking, encryption, or conflict resolution. These are considered premium concerns for a future definitive edition of FinalFinal™.
95
+
96
+ ## Client Testimonials
97
+
98
+ > *"Since adopting `Budget_2025_v4_final_FINAL_ok2_USETHIS.xlsx`, we have reduced version conflicts by 0% but our perceived strategic alignment has increased by 300%."*
99
+ >
100
+ > -- A multinational corporation
101
+
102
+
103
+ > *"We spent a long time choosing between Git and FinalFinal™. What ultimately convinced us to go with FinalFinal™ was the price."*
104
+ >
105
+ > -- A cash-strapped CEO
106
+
107
+ > *"Before FinalFinal™, I wasn't versioning my files at all. Now I am. Sometimes I wonder if things were better before."*
108
+ >
109
+ > -- A weekend entrepreneur
110
+
111
+
112
+ ## Roadmap
113
+
114
+ - **PowerPoint changelog export**: Following the overwhelming success of the PDF exporter and numerous requests from the field, our team is working tirelessly to deliver changelog exports in `.pptx` format. This will allow version history to be presented to the team with full slide transitions.
115
+
116
+ - **Variations**: The introduction of a branching concept. Functionally similar to Git branches, but implemented via subfolders for a better user experience.
117
+
118
+
119
+ ## FAQ
120
+
121
+ **Why doesn't FinalFinal™ use commit messages?**
122
+
123
+ The name speaks for itself. For additional detail, send the file by email to your colleagues or clients. Your inbox *becomes* your changelog. By CC-ing the entire team on each send, you can be confident that everyone is up to date.
124
+
125
+ **Why can't I use `increment()` on a file that hasn't gone through `track()` first?**
126
+
127
+ On any given project, it would be unreasonable to version *every* file. Test files, throwaway scripts, files you are absolutely certain are already in their definitive final form from the first attempt: these do not require tracking.
128
+
129
+ **What if two people modify the file at the same time?**
130
+
131
+ This is called a *collaborative version event*. Both versions are valid. The longer filename wins.
132
+
133
+
134
+ ## Installation & Deployment
135
+
136
+ Download the source code and unzip into your project directory. FinalFinal is supported by all modern deployment systems (dropbox, google drive, wetransfer...)
137
+
138
+ Alternatively, you can use pip or uv, even if it isn't frankly the spirit of the thing.
139
+
140
+ ```bash
141
+ uv add finalfinal
142
+ pip install finalfinal
143
+ ```
144
+
145
+ ## Quick Start
146
+
147
+ The first step is to track the file.
148
+
149
+ > [!WARNING]
150
+ > When tracking a file, a metadata file ``important_notes_DONT_DELETE.docx`` will be created.
151
+ > Do not ask for a more robust system: we already wrote DONT_DELETE in uppercase, YOU are responsible if the versioning framework breaks.
152
+
153
+ ```bash
154
+ finalfinal --path brief.txt --track
155
+ >>> "brief START.txt"
156
+ ```
157
+
158
+ Once the file is tracked, you can start using the increment feature.
159
+
160
+ ```bash
161
+ finalfinal --path brief.txt --increment
162
+ >>> "brief START-updated.txt"
163
+ ```
164
+
165
+ You can adjust the suffix by providing an `increment_type`, and nuance emotional feedback thanks to the `certainty_level` option.
166
+
167
+ ```bash
168
+ # Available increment types are: wip, done, retake, fix, final. Defaults to wip
169
+ finalfinal --path brief.txt --increment --increment_type done --certainty_level 0
170
+ >>> "brief START-updated-Not_Far_From_Decent.txt"
171
+ ```
172
+
173
+ The default certainty level is 1. Below 1 means "unsure", above 1 means "reckless confidence".
174
+
175
+ ```bash
176
+ finalfinal --path brief.txt --increment --increment_type final --certainty_level 99
177
+ >>> "brief START-updated-Not_Far_From_Decent-ABSOLUTELYDEFINITIVE.txt"
178
+ ```
179
+
180
+ Keep everyone up to date thanks to the PDF Export feature.
181
+
182
+ ```bash
183
+ finalfinal --path brief.txt --export_pdf
184
+ >>> "brief_changelog.pdf"
185
+ ```
186
+
187
+ > FinalFinal™ Changelog
188
+ > File: brief.txt
189
+ > Generated: 2026-06-14 at 11:48:08
190
+ > The following represents the complete audit trail of this file. It should not be used in a court of law.
191
+ >
192
+ > Version 1: brief START.txt
193
+ > A human being interacted with this file on 2026-06-14 at 11:36:34. Motivation: unknown.
194
+ >
195
+ > Version 2: brief START-updated.txt
196
+ > This version exists because someone, somewhere, was not satisfied with the previous one.
197
+ >
198
+ > Version 3: brief START-updated-Not_Far_From_Decent.txt
199
+ > The file was improved, allegedly, on 2026-06-14 at 11:46:36.
200
+ >
201
+ > Version 4: brief START-updated-Not_Far_From_Decent-ABSOLUTELYDEFINITIVE.txt
202
+ > Edits were performed. Whether they were improvements is a matter of perspective.
203
+
204
+ When the filename has grown to a length that violates several international conventions, it is time to reset (the metadata file is preserved. It has seen too much to be discarded.)
205
+
206
+ ```bash
207
+ finalfinal --path brief.txt --reset
208
+ >>> brief-NEW_LEAF.txt
209
+ ```
210
+
211
+ ## Contributing
212
+
213
+ Send me your ideas on Discord, i'll consider implementing them.
214
+
215
+ ---
216
+
217
+ <div align="center">
218
+
219
+ **FinalFinal™** - *Because the alternative is learning to use a real version control system.*
220
+
221
+ *© FinalFinal™ Industries. All versions reserved. None of them final.*
222
+
223
+ </div>
@@ -0,0 +1,7 @@
1
+ finalfinal/__init__.py,sha256=Ld2Hmb9u4toTnXRxSBYBz2kAZZOKNuq3SclV9-IYyok,187
2
+ finalfinal/api.py,sha256=t18nJWvXaRhAqesur4YYCn4f-Hp__WtTfmOdtZ8VDzo,32862
3
+ finalfinal-1.0.0.dist-info/licenses/LICENSE,sha256=RBO6PTYOMfrv9U4LyD5VIJaWstZSKWzA2O3i_JEcJkc,1096
4
+ finalfinal-1.0.0.dist-info/WHEEL,sha256=s_zqWxHFEH8b58BCtf46hFCqPaISurdB9R1XJ8za6XI,80
5
+ finalfinal-1.0.0.dist-info/entry_points.txt,sha256=t_VROJgbsSsKq8Iq055bDxhNr8Sd20_j837VURHvlho,48
6
+ finalfinal-1.0.0.dist-info/METADATA,sha256=R1mdpp9bJvZqEio_JANR8NpRew1VvprJknWffP0MEoE,8857
7
+ finalfinal-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.6
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ finalfinal = finalfinal:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tristan Languebien
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.