markovka 0.1.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.
markovka/__init__.py ADDED
File without changes
markovka/chat.py ADDED
@@ -0,0 +1,26 @@
1
+ import json
2
+
3
+
4
+ def fix_encoding(text):
5
+ raw_bytes = text
6
+ fixed = raw_bytes.encode("latin1")
7
+ result = fixed.decode("utf-8")
8
+ return result
9
+
10
+
11
+ def load_chat_messages(path, sender):
12
+ with open(path, encoding="utf-8") as f:
13
+ data = json.load(f)
14
+ messages = list()
15
+ for message in data["messages"]:
16
+ if "sender_name" in message:
17
+ name = fix_encoding(message["sender_name"])
18
+ text = message.get("content")
19
+ if text != None:
20
+ text = fix_encoding(text)
21
+ else:
22
+ name = message.get("senderName")
23
+ text = message.get("text")
24
+ if name == sender and text:
25
+ messages.append(text)
26
+ return messages
@@ -0,0 +1,33 @@
1
+ from markovka.markov import (
2
+ build_ngram_counts,
3
+ generate_text_backoff,
4
+ generate_text_ngram,
5
+ get_tables,
6
+ )
7
+ from markovka.stats import copy_score, share_single_options
8
+
9
+
10
+ def print_result(words, max_n):
11
+ result = get_tables(words, max_n)
12
+ print(len(result[1]))
13
+ print(len(result[2]))
14
+ print(len(result[3]))
15
+
16
+
17
+ def tune_min_options(tables, settings, runs, chunk_set):
18
+ for opt in settings:
19
+ totals = dict()
20
+ score_sum = 0
21
+ for _ in range(runs):
22
+ text, counts = generate_text_backoff(tables, 3, 30, min_options=opt)
23
+ score_sum += copy_score(text, chunk_set, 5)
24
+ for size in counts:
25
+ if size not in totals:
26
+ totals[size] = 0
27
+ totals[size] += counts[size]
28
+ average = score_sum / runs
29
+
30
+ print(f"--- {opt} ---")
31
+ print(totals)
32
+ print(average)
33
+ print(text)
markovka/main.py ADDED
@@ -0,0 +1,184 @@
1
+ import argparse
2
+
3
+ from markovka.chat import load_chat_messages
4
+ from markovka.experiments import print_result, tune_min_options
5
+ from markovka.markov import (
6
+ generate_text_backoff,
7
+ get_tables,
8
+ get_tables_from_texts,
9
+ normalize,
10
+ )
11
+ from markovka.stats import book_chunks, copy_score
12
+ from markovka.storage import load_tables, save_tables
13
+ from markovka.utils import clean_text, get_book_text, remove_header
14
+
15
+
16
+ def main():
17
+ parser = argparse.ArgumentParser()
18
+ parser.add_argument("book", nargs="*", help="file name of the book in books/")
19
+ parser.add_argument(
20
+ "--length", type=int, default=30, help="How many words to generate"
21
+ )
22
+ parser.add_argument(
23
+ "--max-n",
24
+ type=int,
25
+ default=3,
26
+ help="Biggest memory size (order of markov chain). 3 is default and best for books. try 2 for chats.",
27
+ )
28
+ parser.add_argument(
29
+ "--min-options",
30
+ type=int,
31
+ default=2,
32
+ help="best leave at default, copies the source text at 1 and is more gibberish at more than 2",
33
+ )
34
+ parser.add_argument(
35
+ "--count", type=int, default=1, help="Amount of texts to generate"
36
+ )
37
+ parser.add_argument(
38
+ "--experiment",
39
+ action="store_true",
40
+ help="run the copy-score tests and the min_options experiment",
41
+ )
42
+ parser.add_argument(
43
+ "--override-sentences",
44
+ action="store_true",
45
+ help="don't force texts to start with a capital and end with . ! or ?",
46
+ )
47
+ parser.add_argument(
48
+ "--score",
49
+ action="store_true",
50
+ help="Show how much of each text is copied from the source material",
51
+ )
52
+ parser.add_argument("--save", help="Save the tables to this JSON file")
53
+ parser.add_argument(
54
+ "--load", help="load tables from this JSON file instead of reading books"
55
+ )
56
+ parser.add_argument(
57
+ "--chat", nargs="+", help="one or more chat export files in chats/"
58
+ )
59
+ parser.add_argument(
60
+ "--sender", help="whose messages to learn from (exact name as in the export)"
61
+ )
62
+ parser.add_argument(
63
+ "--score-k",
64
+ type=int,
65
+ default=5,
66
+ help="How many words in a row count as copied, try 3 for chats",
67
+ )
68
+ parser.add_argument("--talk", action="store_true", help="talk with the generator!")
69
+ args = parser.parse_args()
70
+ if not args.book and args.load is None and args.chat is None:
71
+ parser.error("Give at least one book, --chat FILES or --load FILE")
72
+ if args.chat and args.sender is None:
73
+ parser.error("--chat needs --sender NAME")
74
+ if args.sender and args.chat is None:
75
+ parser.error("Did you mean --chat?")
76
+
77
+ if args.load and (args.book or args.chat):
78
+ parser.error("Use either books or --load, not both")
79
+ if args.load and (args.score or args.experiment):
80
+ parser.error("--score and --experiment need the book text not just tables")
81
+ if args.load:
82
+ try:
83
+ tables = load_tables(args.load)
84
+ except FileNotFoundError:
85
+ parser.error(f"file {args.load} not found")
86
+ else:
87
+ book_words = list()
88
+ texts = list()
89
+ for book_name in args.book:
90
+ book_path = f"books/{book_name}"
91
+ try:
92
+ book_text = get_book_text(book_path)
93
+ except FileNotFoundError:
94
+ parser.error(f"Book {book_name} not found")
95
+ no_header = remove_header(book_text)
96
+ cleaned = clean_text(no_header)
97
+ book_words.extend(cleaned.split())
98
+ texts.append(cleaned.split())
99
+ if args.chat:
100
+ for chat_name in args.chat:
101
+ chat_path = f"chats/{chat_name}"
102
+ chat_messages = load_chat_messages(chat_path, args.sender)
103
+ for message in chat_messages:
104
+ book_words.extend(message.split())
105
+ texts.append(message.split())
106
+ tables = get_tables_from_texts(texts, args.max_n)
107
+ max_n = len(tables)
108
+ if args.save:
109
+ save_tables(tables, args.save)
110
+
111
+ if args.score or args.experiment:
112
+ book_chunk_set = book_chunks(book_words, args.score_k)
113
+ score_sum = 0
114
+ if args.talk:
115
+ known = set()
116
+ for key in tables[max_n]:
117
+ known.add(normalize(key[0]))
118
+ while True:
119
+ try:
120
+ user = input("you: ")
121
+ except (KeyboardInterrupt, EOFError):
122
+ print()
123
+ break
124
+ if user == "/quit":
125
+ break
126
+ candidates = list()
127
+ for word in user.split():
128
+ if normalize(word) in known:
129
+ candidates.append(word)
130
+ if not candidates:
131
+ start = None
132
+ else:
133
+ start = max(candidates, key=len)
134
+ text, counts = generate_text_backoff(
135
+ tables,
136
+ max_n,
137
+ args.length,
138
+ min_options=args.min_options,
139
+ sentences=not args.override_sentences,
140
+ start_word=start,
141
+ )
142
+ print(f"bot: {text}")
143
+ else:
144
+ for _ in range(args.count):
145
+ text, counts = generate_text_backoff(
146
+ tables,
147
+ max_n,
148
+ args.length,
149
+ min_options=args.min_options,
150
+ sentences=not args.override_sentences,
151
+ )
152
+ print(text)
153
+ if (
154
+ not args.override_sentences
155
+ and text[-1].endswith((".", "!", "?")) == False
156
+ ):
157
+ print(
158
+ "Error! Couldn't generate text ending with a sentence ending mark. Try again, or run with --override-sentences"
159
+ )
160
+ print("-------------")
161
+ if args.score:
162
+ score = copy_score(text, book_chunk_set, args.score_k)
163
+ score_sum += score
164
+ percent = round(score * 100)
165
+ print(f"copied: {percent}%")
166
+ if args.score:
167
+ print("average copied")
168
+ print(round(score_sum / args.count * 100))
169
+
170
+ if args.experiment:
171
+ print(len(book_chunk_set))
172
+ print(len(book_words))
173
+ test_text = """On an exceptionally hot evening early in July a young man came out of
174
+ the garret in which he lodged in S. Place and walked slowly, as though
175
+ in hesitation,"""
176
+ print("--- SANITY TEST ---")
177
+ print(copy_score(test_text, book_chunk_set, 5))
178
+ print("--- ACTUAL TEST --- ")
179
+ print(copy_score(text, book_chunk_set, 5))
180
+ tune_min_options(tables, range(1, 6), 30, book_chunk_set)
181
+
182
+
183
+ if __name__ == "__main__":
184
+ main()
markovka/markov.py ADDED
@@ -0,0 +1,125 @@
1
+ import random
2
+
3
+
4
+ # build the table
5
+ def build_ngram_counts(words, n, counts=None):
6
+ if counts == None:
7
+ ngram_counts = {}
8
+ else:
9
+ ngram_counts = counts
10
+ for i in range(0, len(words) - n):
11
+ current_key = tuple(words[i : i + n])
12
+ next_word = words[i + n]
13
+ if current_key not in ngram_counts:
14
+ ngram_counts[current_key] = dict()
15
+ if next_word not in ngram_counts[current_key]:
16
+ ngram_counts[current_key][next_word] = 0
17
+
18
+ ngram_counts[current_key][next_word] += 1
19
+ return ngram_counts
20
+
21
+
22
+ # generating text
23
+ def generate_text_ngram(table, length):
24
+ all_keys = list(table.keys())
25
+ current = random.choice(all_keys)
26
+ generated = list(current)
27
+ n = len(current)
28
+
29
+ for i in range(length):
30
+ if current not in table:
31
+ current = random.choice(all_keys)
32
+ options = list(table[current].keys())
33
+ weights = list(table[current].values())
34
+ next_word = random.choices(options, weights=weights)[0]
35
+ generated.append(next_word)
36
+ current = tuple(generated[-n:])
37
+ result_seperator = " "
38
+ result = result_seperator.join(generated)
39
+ return result
40
+
41
+
42
+ def generate_text_backoff(
43
+ tables, max_n, length, min_options=2, sentences=True, start_word=None
44
+ ):
45
+ all_keys = list(tables[max_n].keys())
46
+ start_keys = list()
47
+ for key in all_keys:
48
+ if key[0][0].isupper():
49
+ start_keys.append(key)
50
+ if len(start_keys) == 0:
51
+ start_keys = all_keys
52
+ if sentences == False:
53
+ start_keys = all_keys
54
+ if start_word != None:
55
+ matching = list()
56
+ for key in all_keys:
57
+ if key[0] == start_word:
58
+ matching.append(key)
59
+ if len(matching) > 0:
60
+ start_keys = matching
61
+ current = random.choice(start_keys)
62
+ generated = list(current)
63
+ sizes_used = dict()
64
+ if sentences == True:
65
+ max_rounds = length * 3
66
+ else:
67
+ max_rounds = length
68
+ for i in range(max_rounds):
69
+ picked = None
70
+ for n in range(max_n, 0, -1):
71
+ key = tuple(generated[-n:])
72
+ table = tables[n]
73
+ if key in table and (len(table[key]) >= min_options or n == 1):
74
+ options = list(table[key].keys())
75
+ weights = list(table[key].values())
76
+ picked = random.choices(options, weights=weights)[0]
77
+ if n not in sizes_used:
78
+ sizes_used[n] = 0
79
+ sizes_used[n] += 1
80
+ break
81
+ if picked is None:
82
+ single_word_table = tables[1]
83
+ keys = single_word_table.keys()
84
+ key_list = list(keys)
85
+ random_key = random.choice(key_list)
86
+ picked = random_key[0]
87
+ if "jump" not in sizes_used:
88
+ sizes_used["jump"] = 0
89
+ sizes_used["jump"] += 1
90
+ generated.append(picked)
91
+ if sentences and i + 1 >= length and generated[-1].endswith((".", "!", "?")):
92
+ break
93
+ result_seperator = " "
94
+ result = result_seperator.join(generated)
95
+ return result, sizes_used
96
+
97
+
98
+ def get_tables(words, max_n):
99
+ tables = dict()
100
+ for n in range(1, max_n + 1):
101
+ tables[n] = build_ngram_counts(words, n)
102
+ return tables
103
+
104
+
105
+ def get_tables_from_texts(texts, max_n):
106
+ tables = dict()
107
+ for n in range(1, max_n + 1):
108
+ tables[n] = dict()
109
+ for words in texts:
110
+ for n in range(1, max_n + 1):
111
+ build_ngram_counts(words, n, tables[n])
112
+ return tables
113
+
114
+
115
+ def print_result(words, max_n):
116
+ result = get_tables(words, max_n)
117
+ print(len(result[1]))
118
+ print(len(result[2]))
119
+ print(len(result[3]))
120
+
121
+
122
+ def normalize(word):
123
+ lowercase = word.lower()
124
+ stripped = lowercase.strip(".,!?")
125
+ return stripped
markovka/stats.py ADDED
@@ -0,0 +1,30 @@
1
+ def share_single_options(table):
2
+ single_count = 0
3
+ for followers in table.values():
4
+ if len(followers) == 1:
5
+ single_count += 1
6
+ return single_count / len(table)
7
+
8
+
9
+ def book_chunks(words, k):
10
+ chunks = set()
11
+ last_start = len(words) - k
12
+ for i in range(last_start + 1):
13
+ chunk = tuple(words[i : i + k])
14
+ chunks.add(chunk)
15
+ return chunks
16
+
17
+
18
+ def copy_score(text, chunk_set, k):
19
+ text_words = text.split()
20
+ all_chunks = 0
21
+ found_chunk = 0
22
+ last_start = len(text_words) - k
23
+ for i in range(last_start + 1):
24
+ chunk = tuple(text_words[i : i + k])
25
+ all_chunks += 1
26
+ if chunk in chunk_set:
27
+ found_chunk += 1
28
+ if all_chunks == 0:
29
+ return 0.0
30
+ return found_chunk / all_chunks
markovka/storage.py ADDED
@@ -0,0 +1,30 @@
1
+ import json
2
+
3
+
4
+ def save_tables(tables, path):
5
+ json_ready = dict()
6
+ key_seperator = " "
7
+ for n in tables:
8
+ table = tables[n]
9
+ string_table = dict()
10
+ for key in table:
11
+ string_key = key_seperator.join(key)
12
+ string_table[string_key] = table[key]
13
+ json_ready[n] = string_table
14
+ with open(path, "w", encoding="utf-8") as f:
15
+ json.dump(json_ready, f, ensure_ascii=False)
16
+
17
+
18
+ def load_tables(path):
19
+ with open(path, encoding="utf-8") as f:
20
+ loaded = json.load(f)
21
+ tables = dict()
22
+ for n_string in loaded:
23
+ n = int(n_string)
24
+ string_table = loaded[n_string]
25
+ table = dict()
26
+ for string_key in string_table:
27
+ key = tuple(string_key.split())
28
+ table[key] = string_table[string_key]
29
+ tables[n] = table
30
+ return tables
markovka/utils.py ADDED
@@ -0,0 +1,30 @@
1
+ import random
2
+
3
+
4
+ def remove_header(text):
5
+ try:
6
+ start_book = text.index("START OF THE PROJECT GUTENBERG EBOOK")
7
+ marker_end = text.index("***", start_book)
8
+ end_book = text.index("*** END OF")
9
+ no_header = text[marker_end + 3 : end_book]
10
+ return no_header
11
+ except ValueError:
12
+ return text
13
+
14
+
15
+ def get_book_text(path):
16
+ with open(path) as f:
17
+ return f.read()
18
+
19
+
20
+ def clean_text(text):
21
+ lines = text.splitlines()
22
+ kept = list()
23
+ for line in lines:
24
+ stripped = line.strip()
25
+ if stripped.startswith("CHAPTER") or stripped.startswith("PART"):
26
+ continue
27
+ kept.append(stripped)
28
+ result = "\n".join(kept)
29
+ no_april_underscores = result.replace("_", "")
30
+ return no_april_underscores
@@ -0,0 +1,154 @@
1
+ Metadata-Version: 2.4
2
+ Name: markovka
3
+ Version: 0.1.0
4
+ Summary: Markov chain text generator with option of importing messenger chats and books as source material.
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Dynamic: license-file
9
+
10
+ # Markov chain generator
11
+
12
+ [![forthebadge](https://forthebadge.com/badges/0-percent-optimized.svg)](https://forthebadge.com)
13
+
14
+ A command-line Markov chain text generator, that learnsf rom a book and writes new text in its style.
15
+
16
+ > The shy, unknown street? I shall be simply a puppy, and a pack of perspiration stood open. Peasants stood before his
17
+
18
+ 1. it reads a book and builds tables of which words follow which 1, 2 or 3-wordsequences
19
+ 2. generates text by picking the next word randomly, weighted by how often it appeared
20
+ 3. uses longest memory when there's a real choice and uses backoff to fall back to shorter memory when there's only one option to avoid plagiarizing the source text
21
+ 4. an originality checker that measures how much of the output is copied word for word
22
+
23
+ ## Requirements
24
+
25
+ - Python 3 (no exterlan packages; only the standard library)
26
+
27
+ ## Usage
28
+
29
+ Put a plain-text book from [Project Gutenberg](https://www.gutenberg.org) in the `books/` folder, then:
30
+
31
+ ```bash
32
+ python3 main.py <book_file> [options]
33
+ ```
34
+
35
+ | Option | Default | Description |
36
+ | ---------------------- | ------- | ---------------------------------------------------------------------------------------------- |
37
+ | `--length` | 30 | How many words to generate |
38
+ | `--max-n` | 3 | Biggest memory size (n-gram order) |
39
+ | `--min-options` | 2 | Backoff threshold: minimum number of possible next words before falling back to shorter memory |
40
+ | `--count` | 1 | How many texts to generate |
41
+ | `--experiment` | off | Run the copy-score tests and the min_options experiment |
42
+ | `--override-sentences` | off | Don't force texts to start with a capital letter and end with . ! or ? |
43
+ | `--score` | off | Show how much of each text is copied word for word from the source material |
44
+
45
+ ### Examples
46
+
47
+ ```bash
48
+ python3 main.py crimeandpunishment.txt
49
+ ```
50
+
51
+ ```bash
52
+ wretched little beast was still life and strength that moment without this three roubles; I may have had nothing to do with you. I do no exactly.” “Do tell you something more essential
53
+ ```
54
+
55
+ ```bash
56
+ python3 main.py crimeandpunishment.txt --count 3 --length 50
57
+ ```
58
+
59
+ ```bash
60
+ spoke rather loud noise and animated conversation with a sharp little nose off with my sister--the day before yesterday.’ ‘And where did you go, will all keep him on the score of the old, dirty, by this. Then he remembered quite different. “Yes, Mr. Captain, and I am an ass; forgive myself for
61
+ why he went,” Raskolnikov by way will sing ‘My Village’ only, for the smart lady who was so unfortunate girl were asking your _fiancé_ in and weighed upon him. Here is some water. The feeling that it was like the latch on an empty tea-glass on a staircase, he mounted to tell you
62
+ scripture, too,” she was growing up, moved a night cap with new mourning had done _that_, could never bear that hardly audibly and God has smitten you, has no skill in such attire. Suddenly he withdrew his short-sighted eyes. You might well not such a fool I was afraid of!” Porfiry Petrovitch, receiving
63
+ ```
64
+
65
+ ````bash
66
+ python3 main.py crimeandpunishment.txt --experiment```
67
+ ````
68
+
69
+ ```bash
70
+ away in her will, ha-ha!). And she would not overlook anything. For the family who wanted a handkerchief, had turned the pocket is honourable. The vast quantities he said that he would not
71
+ 202521
72
+ 203612
73
+ --- SANITY TEST ---
74
+ 1.0
75
+ --- ACTUAL TEST ---
76
+ 0.034482758620689655
77
+ --- 1 ---
78
+ {3: 900}
79
+ 0.9045977011494255
80
+ corner was disproportionately obtuse. There was scarcely any furniture in the big room: in the corner and several chairs--all government furniture, of polished yellow wood. In the further wall there was a time....
81
+ --- 2 ---
82
+ {1: 380, 2: 361, 3: 159}
83
+ 0.08275862068965514
84
+ They’ve taken them food... I spoke about her mistrustfully, but that probably from his face. “And here... however queer, almost sullen insistence. “You’re right: she put them all so fond of Sonia? Sonia
85
+ --- 3 ---
86
+ {1: 509, 2: 313, 3: 78}
87
+ 0.051724137931034475
88
+ this hesitating question, Sonia....” He stopped for me...” He rang the glass, put a stop here was the gate. She read loudly, cold with her own account, hasn’t said suddenly. “He is himself,”
89
+ --- 4 ---
90
+ {1: 555, 2: 288, 3: 57}
91
+ 0.035632183908045977
92
+ and went in. He looked carefully and exactly what the doctor says!” Katerina Ivanovna, pale, thin spare the bridge. He became at parting for a poor student, or hunchback, I forgot! A plain,
93
+ --- 5 ---
94
+ {1: 599, 2: 266, 3: 35}
95
+ 0.028735632183908046
96
+ “but what help the habit he said this with some other general’s.... Only just getting home in my opinions.... You know my opinion, and describing a change it, the business, and then to
97
+ ```
98
+
99
+ ```bash
100
+ python3 main.py crimeandpunishment.txt --score
101
+ ```
102
+
103
+ ```bash
104
+ After brief hesitation at the police office? Where’s the greatest suspicion of any folly.” They simply looked rather irregular for that moment when he stood with the axe in the noose, he tried to hang himself, and that I did not ask forgiveness, but he was in a fever of dust of the street.
105
+ copied: 18%
106
+ ```
107
+
108
+ ## Results
109
+
110
+ Tested on _Crime and Punishment_ (about 200k words).
111
+
112
+ **Memory size vs copying** The longer the memory the more often there is one possible next word so the generator just copies the book:
113
+
114
+ | Memory | Keys with only one possible next word |
115
+ | ------- | ------------------------------------- |
116
+ | 1 word | 59% |
117
+ | 2 words | 82% |
118
+ | 3 words | 94% |
119
+
120
+ **Backoff vs copying** Copy score = share of 5-0word chunks in the output that appear word for word in the book (average of 30 texts per setting):
121
+
122
+ | min_options | 3-word memory used | Copy score |
123
+ | -------------- | ------------------ | ---------- |
124
+ | 1 (no backoff) | 100% | 91% |
125
+ | 2 | 16% | 7.5% |
126
+ | 3 | 10% | 6.1% |
127
+ | 4 | 5% | 2.6% |
128
+ | 5 | 6% | 2.6% |
129
+
130
+ backoff cuts the copying form 91% to about 7.5. above 2 copying barely drops and the text gets more mashed up. thats why 2 is the default
131
+
132
+ ## Running tests
133
+
134
+ - Create and activate a .venv
135
+ - install pytest
136
+ - run pytest
137
+ Thet ests cover tables, copy score, header removal, cleaning and sentence boundiaries
138
+
139
+ ## Known Limitations
140
+
141
+ - Sentence ends inside quotes like "Good-bye!" arent recognised as sentence ends
142
+ - a sentence start is detected by a capital letter so names in the middle of a sentence can also be piocked as starts
143
+ - in rare cases no sentence end is found within the limit and the program prints a warning
144
+ - for text without capitals or punctuation like chats use --override-sentences
145
+
146
+ ## Roadmap
147
+
148
+ - Generating text from other data sources like JSON
149
+ - Cleaning the source text depending on the type
150
+ - Saving tables to a file
151
+
152
+ ## Credits
153
+
154
+ - Book source: Project gutenberg, shoutout :)
@@ -0,0 +1,14 @@
1
+ markovka/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ markovka/chat.py,sha256=4_N1y2outTKD_nBU7AKbFCpjvibFExV5ivRMzuejT5A,716
3
+ markovka/experiments.py,sha256=HY8d7sf2KbkJqboFAmFjYtEldghM6gkAg5HVfX47XJs,917
4
+ markovka/main.py,sha256=0WO9taaFg5sd5SUKHHVcyj8AsL2PBm-WbydxpMfl6-0,6721
5
+ markovka/markov.py,sha256=8435-xdR3F_7J8fioQAfYFwxEAYaTIW9Ft-AQ8bXjZg,3770
6
+ markovka/stats.py,sha256=DWsaQBfoPZqEdgGVOdJfS4AQcPSi6b8yWjDVIzp9Kc0,778
7
+ markovka/storage.py,sha256=qde38LiOxMNoEgaN1NV6Omq2waHl15JrSiQQHDX4R2M,848
8
+ markovka/utils.py,sha256=Jg-hOBBGt407NY0ZFL1RZjgmLhXK9NlDwKpU70EITr4,778
9
+ markovka-0.1.0.dist-info/licenses/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
10
+ markovka-0.1.0.dist-info/METADATA,sha256=d67o7vcJGKvScxDilr3LGiT4wpTR-NK8VGg-iHq0W2w,7626
11
+ markovka-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
12
+ markovka-0.1.0.dist-info/entry_points.txt,sha256=GBR5jWAcYF-xI2uxWvHPHEPF61KqGIA-fA_zkwnTU3A,48
13
+ markovka-0.1.0.dist-info/top_level.txt,sha256=zqLraDoQWUOQXZl9nJ5tAQm60-EWRwSzctgELrtUXUU,9
14
+ markovka-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ markovka = markovka.main:main
@@ -0,0 +1,24 @@
1
+ This is free and unencumbered software released into the public domain.
2
+
3
+ Anyone is free to copy, modify, publish, use, compile, sell, or
4
+ distribute this software, either in source code form or as a compiled
5
+ binary, for any purpose, commercial or non-commercial, and by any
6
+ means.
7
+
8
+ In jurisdictions that recognize copyright laws, the author or authors
9
+ of this software dedicate any and all copyright interest in the
10
+ software to the public domain. We make this dedication for the benefit
11
+ of the public at large and to the detriment of our heirs and
12
+ successors. We intend this dedication to be an overt act of
13
+ relinquishment in perpetuity of all present and future rights to this
14
+ software under copyright law.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ For more information, please refer to <https://unlicense.org>
@@ -0,0 +1 @@
1
+ markovka